├── .github ├── CODEOWNERS ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE.md ├── README.md ├── src ├── content-types.xml ├── error.rs ├── lib.rs ├── model │ ├── mesh.rs │ └── mod.rs ├── read.rs ├── rels.xml └── write.rs └── tests ├── model.rs └── roundtrip.rs /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hannobraun 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [hannobraun] 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Check out repository 19 | uses: actions/checkout@v4 20 | - name: Check formatting 21 | run: cargo fmt -- --check 22 | - name: Run `cargo clippy` 23 | run: cargo clippy --all-features -- -D warnings 24 | - name: Run `cargo test` 25 | run: cargo test -- --nocapture 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cargo 2 | /target 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnPaste": true, 3 | "editor.formatOnSave": true 4 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.7.0 (2025-01-31) 4 | 5 | - Update dependencies ([#58]) 6 | - **Breaking change:** Upgrade to `quick-xml` 0.37 ([#61]) 7 | 8 | [#58]: https://github.com/hannobraun/3mf-rs/pull/58 9 | [#61]: https://github.com/hannobraun/3mf-rs/pull/61 10 | 11 | ## v0.6.0 (2024-09-18) 12 | 13 | - Update dependencies ([#46], [#48], [#49], [#50], [#51], [#52]) 14 | - **Breaking change:** Fix crash when parsing `` ([#54]) 15 | 16 | [#46]: https://github.com/hannobraun/3mf-rs/pull/46 17 | [#48]: https://github.com/hannobraun/3mf-rs/pull/48 18 | [#49]: https://github.com/hannobraun/3mf-rs/pull/49 19 | [#50]: https://github.com/hannobraun/3mf-rs/pull/50 20 | [#51]: https://github.com/hannobraun/3mf-rs/pull/51 21 | [#52]: https://github.com/hannobraun/3mf-rs/pull/52 22 | [#54]: https://github.com/hannobraun/3mf-rs/pull/54 23 | 24 | ## v0.5.0 (2024-02-14) 25 | 26 | - Add support for reading 3MF files ([#28], [#32]) 27 | - Don't require actual files when reading/writing 3MF ([#31]) 28 | - Accept `Into` in `write` ([#33]) 29 | - Update dependencies ([#34], [#36], [#37], [#39]) 30 | - Update README ([#40]) 31 | 32 | [#28]: https://github.com/hannobraun/3mf-rs/pull/28 33 | [#31]: https://github.com/hannobraun/3mf-rs/pull/31 34 | [#32]: https://github.com/hannobraun/3mf-rs/pull/32 35 | [#33]: https://github.com/hannobraun/3mf-rs/pull/33 36 | [#34]: https://github.com/hannobraun/3mf-rs/pull/34 37 | [#36]: https://github.com/hannobraun/3mf-rs/pull/36 38 | [#37]: https://github.com/hannobraun/3mf-rs/pull/37 39 | [#39]: https://github.com/hannobraun/3mf-rs/pull/39 40 | [#40]: https://github.com/hannobraun/3mf-rs/pull/40 41 | 42 | ## v0.4.0 (2023-02-17) 43 | 44 | - Switch to Serde for writing XML ([#22]) 45 | 46 | [#22]: https://github.com/hannobraun/3mf-rs/pull/22 47 | 48 | ## v0.3.1 (2022-05-24) 49 | 50 | - Remove unused bzip2 dependency ([#12], [#13]) 51 | 52 | [#12]: https://github.com/hannobraun/3mf-rs/pull/12 53 | [#13]: https://github.com/hannobraun/3mf-rs/pull/13 54 | 55 | ## v0.3.0 (2022-04-13) 56 | 57 | - Re-export `write::Error` from root module ([#9]) 58 | - Accept `&Path` instead of `PathBuf` in `write` [#10] 59 | 60 | [#9]: https://github.com/hannobraun/3mf-rs/pull/9 61 | [#10]: https://github.com/hannobraun/3mf-rs/pull/10 62 | 63 | ## v0.2.0 (2021-11-20) 64 | 65 | - Use `f64` to represent numbers ([#6]) 66 | 67 | [#6]: https://github.com/hannobraun/3mf-rs/pull/6 68 | 69 | ## v0.1.0 (2021-10-24) 70 | 71 | Initial release. 72 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "threemf" 3 | version = "0.7.0" 4 | 5 | edition = "2021" 6 | rust-version = "1.56" 7 | 8 | description = "3MF (3D Manufacturing Format) file format support" 9 | license = "0BSD" 10 | keywords = ["3MF", "CAD", "slicer", "triangle", "mesh"] 11 | categories = ["encoding", "rendering::data-formats"] 12 | 13 | readme = "README.md" 14 | repository = "https://github.com/hannobraun/3mf-rs" 15 | 16 | 17 | [dependencies] 18 | quick-xml = { version = "0.37.0", features = ["serialize"] } 19 | serde = { version = "1.0.152", features = ["derive"] } 20 | thiserror = "2.0.3" 21 | 22 | [dependencies.zip] 23 | version = "4.0.0" 24 | default-features = false 25 | features = ["deflate"] 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Zero-Clause BSD License 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 3MF (3D Manufacturing Format) support for Rust [![crates.io](https://img.shields.io/crates/v/threemf.svg)](https://crates.io/crates/threemf) [![Documentation](https://docs.rs/threemf/badge.svg)](https://docs.rs/threemf) [![CI](https://github.com/hannobraun/3mf-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/hannobraun/3mf-rs/actions/workflows/ci.yml) 2 | 3 | ## About 4 | 5 | This library provides support for [3MF] files to programs written in the Rust 6 | programming language. 3MF is a file format commonly used for 3D printing. It is 7 | typically exported from a CAD program, and imported to a slicer. 8 | 9 | [3MF]: https://en.wikipedia.org/wiki/3D_Manufacturing_Format 10 | 11 | ## Status 12 | 13 | Functionality is limited, but what is currently there seems to work well. This 14 | library is used by (and has been extracted from) [Fornjot]. 15 | 16 | [Fornjot]: https://github.com/hannobraun/fornjot 17 | 18 | ## License 19 | 20 | This project is open source software, licensed under the terms of the 21 | [Zero Clause BSD License] (0BSD, for short). This basically means you can do 22 | anything with the software, without any restrictions, but you can't hold the 23 | authors liable for problems. 24 | 25 | See [LICENSE.md] for all details. 26 | 27 | [Zero Clause BSD License]: https://opensource.org/licenses/0BSD 28 | [LICENSE.md]: https://github.com/hannobraun/3mf-rs/blob/main/LICENSE.md 29 | -------------------------------------------------------------------------------- /src/content-types.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | use zip::result::ZipError; 3 | 4 | /// An error that can occur while writing a 3MF file 5 | #[derive(Debug, Error)] 6 | pub enum Error { 7 | /// I/O error while writing 3MF file 8 | #[error("I/O error while importing/exporting to 3MF file")] 9 | Io(#[from] std::io::Error), 10 | 11 | /// Error writing ZIP file (3MF files are ZIP files) 12 | #[error("Error writing ZIP file (3MF files are ZIP files)")] 13 | Zip(#[from] ZipError), 14 | 15 | /// Error Deserializing internal 3MF XML structure 16 | #[error("Deserialization error from xml reading")] 17 | XMLDe(#[from] quick_xml::DeError), 18 | 19 | /// Error Serializing internal 3MF XML structure 20 | #[error("Serialization error from xml writing")] 21 | XMLSe(#[from] quick_xml::SeError), 22 | } 23 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # 3MF (3D Manufacturing Format) support for Rust 2 | //! 3 | //! This library provides support for [3MF] files to programs written in the 4 | //! Rust programming language. 3MF is a file format commonly used for 3D 5 | //! printing. It is typically exported from a CAD program, and imported to a 6 | //! slicer. 7 | //! 8 | //! So far, functionality is limited to writing 3MF files, and only the most 9 | //! basic features of 3MF are supported. Adding support for reading 3MF files, 10 | //! and for more features of the 3MF format is very desirable, and any 11 | //! contributions toward that are very welcome. 12 | //! 13 | //! [3MF]: https://en.wikipedia.org/wiki/3D_Manufacturing_Format 14 | //! 15 | //! 16 | //! ## Further Reading 17 | //! 18 | //! See [3MF specification] and [Open Packaging Conventions]. 19 | //! 20 | //! [3MF specification]: https://3mf.io/specification/ 21 | //! [Open Packaging Conventions]: https://standards.iso.org/ittf/PubliclyAvailableStandards/c061796_ISO_IEC_29500-2_2012.zip 22 | 23 | pub mod error; 24 | pub mod model; 25 | pub mod read; 26 | pub mod write; 27 | 28 | pub use self::{error::Error, model::Mesh, read::read, write::write}; 29 | -------------------------------------------------------------------------------- /src/model/mesh.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// A triangle mesh 4 | /// 5 | /// This is a very basic types that lacks any amenities for constructing it or 6 | /// for iterating over its data. 7 | /// 8 | /// This is by design. Providing a generally usable and feature-rich triangle 9 | /// mesh type is out of scope for this library. It is expected that users of 10 | /// this library will use their own mesh type anyway, and the simplicity of 11 | /// `TriangleMesh` provides an easy target for conversion from such a type. 12 | #[derive(Serialize, Deserialize, PartialEq, Clone)] 13 | pub struct Mesh { 14 | /// The vertices of the mesh 15 | /// 16 | /// This defines the vertices that are part of the mesh, but not the mesh's 17 | /// structure. See the `triangles` field. 18 | pub vertices: Vertices, 19 | 20 | /// The triangles that make up the mesh 21 | /// 22 | /// Each triangle consists of indices that refer back to the `vertices` 23 | /// field. 24 | pub triangles: Triangles, 25 | } 26 | 27 | /// A list of vertices, as a struct mainly to comply with easier serde xml 28 | #[derive(Serialize, Deserialize, PartialEq, Clone)] 29 | pub struct Vertices { 30 | #[serde(default)] 31 | pub vertex: Vec, 32 | } 33 | 34 | /// A list of triangles, as a struct mainly to comply with easier serde xml 35 | #[derive(Serialize, Deserialize, PartialEq, Clone)] 36 | pub struct Triangles { 37 | #[serde(default)] 38 | pub triangle: Vec, 39 | } 40 | 41 | /// A vertex in a triangle mesh 42 | #[derive(Serialize, Deserialize, PartialEq, Clone)] 43 | pub struct Vertex { 44 | #[serde(rename = "@x")] 45 | pub x: f64, 46 | #[serde(rename = "@y")] 47 | pub y: f64, 48 | #[serde(rename = "@z")] 49 | pub z: f64, 50 | } 51 | 52 | /// A triangle in a triangle mesh 53 | /// 54 | /// The triangle consists of indices that refer to the vertices of the mesh. See 55 | /// [`TriangleMesh`]. 56 | #[derive(Serialize, Deserialize, PartialEq, Clone)] 57 | pub struct Triangle { 58 | #[serde(rename = "@v1")] 59 | pub v1: usize, 60 | #[serde(rename = "@v2")] 61 | pub v2: usize, 62 | #[serde(rename = "@v3")] 63 | pub v3: usize, 64 | } 65 | -------------------------------------------------------------------------------- /src/model/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod mesh; 2 | pub use mesh::*; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Serialize, Deserialize)] 7 | #[serde(rename_all = "lowercase")] 8 | pub struct Model { 9 | #[serde(rename = "@xmlns", default)] 10 | pub xmlns: String, 11 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 12 | pub metadata: Vec, 13 | pub resources: Resources, 14 | pub build: Build, 15 | #[serde(rename = "@unit", default)] 16 | pub unit: Unit, 17 | } 18 | 19 | /// Model measurement unit, default is millimeter 20 | #[derive(Serialize, Deserialize)] 21 | #[serde(rename_all = "lowercase")] 22 | pub enum Unit { 23 | Micron, 24 | Millimeter, 25 | Centimeter, 26 | Inch, 27 | Foot, 28 | Meter, 29 | } 30 | 31 | #[derive(Serialize, Deserialize)] 32 | pub struct Metadata { 33 | #[serde(rename = "@name")] 34 | pub name: String, 35 | #[serde(rename = "$value")] 36 | pub value: Option, 37 | } 38 | 39 | #[derive(Serialize, Deserialize, Default)] 40 | pub struct Resources { 41 | #[serde(default)] 42 | pub object: Vec, 43 | #[serde(default, skip_serializing_if = "Option::is_none")] 44 | pub basematerials: Option<()>, 45 | } 46 | 47 | #[derive(Serialize, Deserialize)] 48 | #[serde(rename_all = "lowercase")] 49 | pub struct Object { 50 | #[serde(rename = "@id")] 51 | pub id: usize, 52 | #[serde(rename = "@partnumber", skip_serializing_if = "Option::is_none")] 53 | pub partnumber: Option, 54 | #[serde(rename = "@name", skip_serializing_if = "Option::is_none")] 55 | pub name: Option, 56 | #[serde(rename = "@pid", skip_serializing_if = "Option::is_none")] 57 | pub pid: Option, 58 | 59 | #[serde(skip_serializing_if = "Option::is_none")] 60 | pub mesh: Option, 61 | 62 | #[serde(skip_serializing_if = "Option::is_none")] 63 | pub components: Option, 64 | } 65 | 66 | #[derive(Serialize, Deserialize)] 67 | pub struct Components { 68 | pub component: Vec, 69 | } 70 | 71 | #[derive(Serialize, Deserialize)] 72 | pub struct Component { 73 | #[serde(rename = "@objectid")] 74 | pub objectid: usize, 75 | #[serde(rename = "@transform", skip_serializing_if = "Option::is_none")] 76 | pub transform: Option<[f64; 12]>, 77 | } 78 | #[derive(Serialize, Deserialize, Default)] 79 | pub struct Build { 80 | #[serde(default)] 81 | pub item: Vec, 82 | } 83 | 84 | #[derive(Serialize, Deserialize)] 85 | pub struct Item { 86 | #[serde(rename = "@objectid")] 87 | pub objectid: usize, 88 | #[serde(rename = "@transform", skip_serializing_if = "Option::is_none")] 89 | pub transform: Option<[f64; 12]>, 90 | #[serde(rename = "@partnumber", skip_serializing_if = "Option::is_none")] 91 | pub partnumber: Option, 92 | } 93 | 94 | impl Default for Model { 95 | fn default() -> Self { 96 | Self { 97 | xmlns: "http://schemas.microsoft.com/3dmanufacturing/core/2015/02".to_owned(), 98 | metadata: Vec::new(), 99 | resources: Resources::default(), 100 | build: Build::default(), 101 | unit: Unit::default(), 102 | } 103 | } 104 | } 105 | 106 | impl Default for Unit { 107 | fn default() -> Self { 108 | Self::Millimeter 109 | } 110 | } 111 | 112 | impl From for Model { 113 | fn from(mesh: Mesh) -> Self { 114 | let object = Object { 115 | id: 1, 116 | partnumber: None, 117 | name: None, 118 | pid: None, 119 | mesh: Some(mesh), 120 | components: None, 121 | }; 122 | let resources = Resources { 123 | object: vec![object], 124 | basematerials: None, 125 | }; 126 | let build = Build { 127 | item: vec![Item { 128 | objectid: 1, 129 | transform: None, 130 | partnumber: None, 131 | }], 132 | }; 133 | Model { 134 | resources, 135 | build, 136 | ..Default::default() 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/read.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::io::{self, BufReader, Read}; 3 | 4 | use quick_xml::de::Deserializer; 5 | use zip::ZipArchive; 6 | 7 | use crate::model::Model; 8 | use crate::Error; 9 | 10 | /// Read all models from a 3MF reader 11 | pub fn read(reader: R) -> Result, Error> { 12 | let mut zip = ZipArchive::new(reader)?; 13 | let mut models = Vec::new(); 14 | 15 | for i in 0..zip.len() { 16 | let file = zip.by_index(i)?; 17 | if file.name().ends_with(".model") { 18 | let mut de = Deserializer::from_reader(BufReader::new(file)); 19 | models.push(Model::deserialize(&mut de)?); 20 | } 21 | } 22 | 23 | Ok(models) 24 | } 25 | -------------------------------------------------------------------------------- /src/rels.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /src/write.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, prelude::*}; 2 | 3 | use crate::Error; 4 | use quick_xml::{ 5 | events::{BytesDecl, Event}, 6 | se::Serializer, 7 | Writer, 8 | }; 9 | use serde::Serialize; 10 | 11 | use zip::{write::SimpleFileOptions, ZipWriter}; 12 | 13 | use crate::model::Model; 14 | 15 | /// Write a triangle mesh to a 3MF writer 16 | pub fn write>(writer: W, model: M) -> Result<(), Error> { 17 | let mut archive = ZipWriter::new(writer); 18 | 19 | archive.start_file("[Content_Types].xml", SimpleFileOptions::default())?; 20 | archive.write_all(include_bytes!("content-types.xml"))?; 21 | 22 | archive.start_file("_rels/.rels", SimpleFileOptions::default())?; 23 | archive.write_all(include_bytes!("rels.xml"))?; 24 | 25 | archive.start_file("3D/model.model", SimpleFileOptions::default())?; 26 | 27 | let mut xml = String::new(); 28 | 29 | let mut ser = Serializer::with_root(&mut xml, Some("model"))?; 30 | ser.indent(' ', 2); 31 | model.into().serialize(ser)?; 32 | 33 | let mut xml_writer = Writer::new_with_indent(&mut archive, b' ', 2); 34 | xml_writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("utf-8"), None)))?; 35 | xml_writer.write_indent()?; 36 | xml_writer.into_inner().write_all(xml.as_bytes())?; 37 | 38 | archive.finish()?; 39 | 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /tests/model.rs: -------------------------------------------------------------------------------- 1 | use threemf::model::{Components, Object}; 2 | 3 | #[test] 4 | fn test_object() { 5 | let object_str = r##""##; 6 | let object_de: Object = quick_xml::de::from_str(object_str).unwrap(); 7 | match object_de { 8 | Object { mesh: Some(_), .. } => panic!("No mesh in this object"), 9 | Object { 10 | components: Some(Components { component }), 11 | .. 12 | } => { 13 | assert_eq!(component.len(), 3); 14 | let transform = component.first().unwrap().transform.unwrap(); 15 | assert_eq!(transform[0], 0.0393701); 16 | } 17 | _ => panic!("There should be components"), 18 | } 19 | } 20 | 21 | #[test] 22 | fn test_metadatagroup() { 23 | let object_str = r##" 24 | 25 | 26 | Body 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | "##; 40 | let object_de: Object = quick_xml::de::from_str(object_str).unwrap(); 41 | assert!(object_de.mesh.is_some()); 42 | } 43 | -------------------------------------------------------------------------------- /tests/roundtrip.rs: -------------------------------------------------------------------------------- 1 | use model::{Triangle, Triangles, Vertex, Vertices}; 2 | use std::io::Cursor; 3 | use threemf::{model, Mesh}; 4 | 5 | #[test] 6 | fn roundtrip() { 7 | let vertices = Vertices { 8 | vertex: vec![ 9 | Vertex { 10 | x: 0.0, 11 | y: 0.0, 12 | z: 0.0, 13 | }, 14 | Vertex { 15 | x: 0.0, 16 | y: 2.0, 17 | z: 0.0, 18 | }, 19 | Vertex { 20 | x: 0.0, 21 | y: 1.0, 22 | z: 1.0, 23 | }, 24 | ], 25 | }; 26 | 27 | let triangles = Triangles { 28 | triangle: vec![Triangle { 29 | v1: 0, 30 | v2: 1, 31 | v3: 2, 32 | }], 33 | }; 34 | 35 | let mesh = Mesh { 36 | triangles, 37 | vertices, 38 | }; 39 | 40 | let write_mesh = mesh.clone(); 41 | 42 | let mut buf = Cursor::new(Vec::new()); 43 | 44 | threemf::write(&mut buf, mesh).expect("Error writing mesh"); 45 | let models = threemf::read(&mut buf).expect("Error reading model"); 46 | 47 | if let Some(read_mesh) = &models[0].resources.object[0].mesh { 48 | assert!(read_mesh == &write_mesh); 49 | } 50 | } 51 | --------------------------------------------------------------------------------