1use std::path::Path;
4
5use spatialrust_core::PointCloud;
6
7use crate::IoError;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub enum PointCloudFileFormat {
12 Pcd,
14 Ply,
16 Las,
18 Laz,
20 E57,
22 Copc,
24}
25
26impl PointCloudFileFormat {
27 #[must_use]
29 pub const fn extension(self) -> &'static str {
30 match self {
31 Self::Pcd => "pcd",
32 Self::Ply => "ply",
33 Self::Las => "las",
34 Self::Laz => "laz",
35 Self::E57 => "e57",
36 Self::Copc => "copc.laz",
37 }
38 }
39}
40
41#[must_use]
43pub fn detect_point_cloud_format(path: impl AsRef<Path>) -> Option<PointCloudFileFormat> {
44 let path = path.as_ref();
45 let file_name = path.file_name()?.to_str()?.to_ascii_lowercase();
46 if file_name.ends_with(".copc.laz") || file_name.ends_with(".copc.las") {
47 return Some(PointCloudFileFormat::Copc);
48 }
49
50 let extension = path.extension()?.to_str()?.to_ascii_lowercase();
51 match extension.as_str() {
52 "pcd" => Some(PointCloudFileFormat::Pcd),
53 "ply" => Some(PointCloudFileFormat::Ply),
54 "las" => Some(PointCloudFileFormat::Las),
55 "laz" => Some(PointCloudFileFormat::Laz),
56 "e57" => Some(PointCloudFileFormat::E57),
57 _ => None,
58 }
59}
60
61pub fn read_point_cloud_file(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
63 let path = path.as_ref();
64 let format = detect_point_cloud_format(path).ok_or_else(|| {
65 IoError::Io(format!("unsupported or missing point cloud extension: {}", path.display()))
66 })?;
67 read_point_cloud_file_with_format(path, format)
68}
69
70pub fn read_point_cloud_file_with_format(
72 path: impl AsRef<Path>,
73 format: PointCloudFileFormat,
74) -> Result<PointCloud, IoError> {
75 match format {
76 PointCloudFileFormat::Pcd => read_pcd_file(path),
77 PointCloudFileFormat::Ply => read_ply_file(path),
78 PointCloudFileFormat::Las => read_las_file(path),
79 PointCloudFileFormat::Laz => read_laz_file(path),
80 PointCloudFileFormat::E57 => read_e57_file(path),
81 PointCloudFileFormat::Copc => read_copc_file(path),
82 }
83}
84
85pub fn write_point_cloud_file(path: impl AsRef<Path>, cloud: &PointCloud) -> Result<(), IoError> {
87 let path = path.as_ref();
88 let format = detect_point_cloud_format(path).ok_or_else(|| {
89 IoError::Io(format!("unsupported or missing point cloud extension: {}", path.display()))
90 })?;
91 write_point_cloud_file_with_format(path, cloud, format)
92}
93
94pub fn write_point_cloud_file_with_format(
96 path: impl AsRef<Path>,
97 cloud: &PointCloud,
98 format: PointCloudFileFormat,
99) -> Result<(), IoError> {
100 match format {
101 PointCloudFileFormat::Pcd => write_pcd_file(path, cloud),
102 PointCloudFileFormat::Ply => write_ply_file(path, cloud),
103 PointCloudFileFormat::Las => write_las_file(path, cloud),
104 PointCloudFileFormat::Laz => write_laz_file(path, cloud),
105 PointCloudFileFormat::E57 => write_e57_file(path, cloud),
106 PointCloudFileFormat::Copc => write_copc_file(path, cloud),
107 }
108}
109
110#[cfg(feature = "io-pcd")]
111fn read_pcd_file(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
112 crate::pcd::read_pcd_file(path)
113}
114
115#[cfg(not(feature = "io-pcd"))]
116fn read_pcd_file(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
117 Err(missing_feature_error("io-pcd", path.as_ref()))
118}
119
120#[cfg(feature = "io-ply")]
121fn read_ply_file(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
122 crate::ply::read_ply_file(path)
123}
124
125#[cfg(not(feature = "io-ply"))]
126fn read_ply_file(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
127 Err(missing_feature_error("io-ply", path.as_ref()))
128}
129
130#[cfg(feature = "io-las")]
131fn read_las_file(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
132 crate::las::read_las_file(path)
133}
134
135#[cfg(not(feature = "io-las"))]
136fn read_las_file(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
137 Err(missing_feature_error("io-las", path.as_ref()))
138}
139
140#[cfg(feature = "io-laz")]
141fn read_laz_file(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
142 crate::las::read_las_file(path)
143}
144
145#[cfg(not(feature = "io-laz"))]
146fn read_laz_file(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
147 Err(missing_feature_error("io-laz", path.as_ref()))
148}
149
150#[cfg(feature = "io-e57")]
151fn read_e57_file(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
152 crate::e57::read_e57_file(path)
153}
154
155#[cfg(not(feature = "io-e57"))]
156fn read_e57_file(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
157 Err(missing_feature_error("io-e57", path.as_ref()))
158}
159
160#[cfg(feature = "io-copc")]
161fn read_copc_file(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
162 crate::copc::read_copc_file(path)
163}
164
165#[cfg(not(feature = "io-copc"))]
166fn read_copc_file(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
167 Err(missing_feature_error("io-copc", path.as_ref()))
168}
169
170#[cfg(feature = "io-pcd")]
171fn write_pcd_file(path: impl AsRef<Path>, cloud: &PointCloud) -> Result<(), IoError> {
172 crate::pcd::write_pcd_file(path, cloud, crate::pcd::PcdWriteFormat::Binary)
173}
174
175#[cfg(not(feature = "io-pcd"))]
176fn write_pcd_file(path: impl AsRef<Path>, _cloud: &PointCloud) -> Result<(), IoError> {
177 Err(missing_feature_error("io-pcd", path.as_ref()))
178}
179
180#[cfg(feature = "io-ply")]
181fn write_ply_file(path: impl AsRef<Path>, cloud: &PointCloud) -> Result<(), IoError> {
182 crate::ply::write_ply_file(path, cloud, crate::ply::PlyWriteFormat::BinaryLittleEndian)
183}
184
185#[cfg(not(feature = "io-ply"))]
186fn write_ply_file(path: impl AsRef<Path>, _cloud: &PointCloud) -> Result<(), IoError> {
187 Err(missing_feature_error("io-ply", path.as_ref()))
188}
189
190#[cfg(feature = "io-las")]
191fn write_las_file(path: impl AsRef<Path>, cloud: &PointCloud) -> Result<(), IoError> {
192 crate::las::write_las_file(path, cloud, crate::las::LasWriteFormat::Las)
193}
194
195#[cfg(not(feature = "io-las"))]
196fn write_las_file(path: impl AsRef<Path>, _cloud: &PointCloud) -> Result<(), IoError> {
197 Err(missing_feature_error("io-las", path.as_ref()))
198}
199
200#[cfg(feature = "io-laz")]
201fn write_laz_file(path: impl AsRef<Path>, cloud: &PointCloud) -> Result<(), IoError> {
202 crate::las::write_las_file(path, cloud, crate::las::LasWriteFormat::Laz)
203}
204
205#[cfg(not(feature = "io-laz"))]
206fn write_laz_file(path: impl AsRef<Path>, _cloud: &PointCloud) -> Result<(), IoError> {
207 Err(missing_feature_error("io-laz", path.as_ref()))
208}
209
210#[cfg(feature = "io-e57")]
211fn write_e57_file(path: impl AsRef<Path>, cloud: &PointCloud) -> Result<(), IoError> {
212 crate::e57::write_e57_file(path, cloud)
213}
214
215#[cfg(not(feature = "io-e57"))]
216fn write_e57_file(path: impl AsRef<Path>, _cloud: &PointCloud) -> Result<(), IoError> {
217 Err(missing_feature_error("io-e57", path.as_ref()))
218}
219
220#[cfg(feature = "io-copc")]
221fn write_copc_file(path: impl AsRef<Path>, cloud: &PointCloud) -> Result<(), IoError> {
222 crate::copc::write_copc_file(path, cloud)
223}
224
225#[cfg(not(feature = "io-copc"))]
226fn write_copc_file(path: impl AsRef<Path>, _cloud: &PointCloud) -> Result<(), IoError> {
227 Err(missing_feature_error("io-copc", path.as_ref()))
228}
229
230#[allow(dead_code)] fn missing_feature_error(feature: &str, path: &Path) -> IoError {
232 IoError::Io(format!("reading or writing `{}` requires the `{feature}` feature", path.display()))
233}
234
235#[cfg(test)]
236mod tests {
237 use super::{detect_point_cloud_format, PointCloudFileFormat};
238 use std::path::PathBuf;
239
240 #[test]
241 fn detects_known_extensions() {
242 assert_eq!(detect_point_cloud_format("scan.pcd"), Some(PointCloudFileFormat::Pcd));
243 assert_eq!(
244 detect_point_cloud_format(PathBuf::from("/tmp/cloud.PLY")),
245 Some(PointCloudFileFormat::Ply)
246 );
247 assert_eq!(detect_point_cloud_format("data.xyz"), None);
248 assert_eq!(detect_point_cloud_format("scan.copc.laz"), Some(PointCloudFileFormat::Copc));
249 }
250
251 #[cfg(feature = "io-pcd")]
252 #[test]
253 fn roundtrip_via_auto_dispatch() {
254 use super::{read_point_cloud_file, write_point_cloud_file};
255 use spatialrust_core::PointCloudBuilder;
256
257 let mut builder = PointCloudBuilder::xyz();
258 builder.push_point([1.0, 2.0, 3.0]).unwrap();
259 let cloud = builder.build().unwrap();
260
261 let path =
262 std::env::temp_dir().join(format!("spatialrust_auto_{}.pcd", std::process::id()));
263 write_point_cloud_file(&path, &cloud).unwrap();
264 let loaded = read_point_cloud_file(&path).unwrap();
265 let _ = std::fs::remove_file(path);
266 assert_eq!(loaded.len(), 1);
267 }
268}