Skip to main content

rigidity_io/
lib.rs

1//! Point-cloud input and output.
2//!
3//! The heavy format dependencies are isolated here; the core knows nothing
4//! about files.
5//!
6//! PLY is parsed by our own reader — `ply-rs` has not been updated since
7//! 2020, and the format is simple enough that a six-year-old dependency
8//! costs more than the code does. LAS is read through the `las` crate,
9//! which handles non-trivial headers, coordinate scaling and LAZ
10//! compression.
11
12pub mod csv;
13pub mod e57_format;
14pub mod las_format;
15pub mod pcd;
16pub mod ply;
17pub mod text;
18
19pub use csv::{read_csv, read_poses};
20pub use e57_format::{read_e57, write_e57};
21pub use las_format::{read_las, write_las};
22pub use pcd::{read_pcd, write_pcd};
23pub use ply::{read_ply, write_ply};
24pub use text::{read_text, write_text};
25
26use std::path::Path;
27
28use rigidity_core::PointCloud;
29
30/// Reads a cloud, choosing the format by the file's extension.
31///
32/// The extension is all there is to go on and all anyone uses. A reader
33/// that sniffed the contents would be right more often and would also
34/// silently open a file the person did not mean to open.
35pub fn read(path: &Path) -> Result<PointCloud, IoError> {
36    match extension(path).as_str() {
37        "ply" => read_ply(path),
38        "las" | "laz" => read_las(path),
39        "e57" => read_e57(path),
40        "pcd" => read_pcd(path),
41        "csv" | "txt" => read_text(path),
42        other => Err(IoError::UnknownFormat(other.to_owned())),
43    }
44}
45
46/// Writes a cloud, choosing the format by the file's extension.
47pub fn write(cloud: &PointCloud, path: &Path) -> Result<(), IoError> {
48    match extension(path).as_str() {
49        "ply" => write_ply(cloud, path),
50        "las" | "laz" => write_las(cloud, path),
51        "e57" => write_e57(cloud, path),
52        "pcd" => write_pcd(cloud, path),
53        "csv" | "txt" => write_text(cloud, path),
54        other => Err(IoError::UnknownFormat(other.to_owned())),
55    }
56}
57
58/// Every extension `read` understands, for a file dialog to offer.
59pub const READABLE: &[&str] = &["ply", "las", "laz", "e57", "pcd", "csv", "txt"];
60
61/// Every extension `write` understands.
62///
63/// `csv` joined `txt` rather than being left behind it: the two go through
64/// one writer that differs only in the separator, and a list that offered
65/// one and refused the other would be describing this crate's history
66/// rather than its behaviour.
67pub const WRITABLE: &[&str] = &["ply", "las", "laz", "e57", "pcd", "txt", "csv"];
68
69fn extension(path: &Path) -> String {
70    path.extension()
71        .and_then(|extension| extension.to_str())
72        .unwrap_or_default()
73        .to_ascii_lowercase()
74}
75
76/// Read and write errors.
77#[derive(Debug, thiserror::Error)]
78pub enum IoError {
79    /// A filesystem error.
80    #[error("I/O error: {0}")]
81    Io(#[from] std::io::Error),
82    /// The file does not begin with the `ply` signature.
83    #[error("not a PLY file: the first line is not \"ply\"")]
84    NotPly,
85    /// Unsupported PLY format.
86    #[error("PLY format \"{0}\" is not supported: need ascii or binary_little_endian")]
87    UnsupportedFormat(String),
88    /// The header is malformed.
89    #[error("malformed PLY header: {0}")]
90    BadHeader(String),
91    /// Unknown property type.
92    #[error("unknown PLY property type: \"{0}\"")]
93    UnknownPropertyType(String),
94    /// The `vertex` element carries no coordinates.
95    #[error("the vertex element is missing the x, y, z properties")]
96    MissingCoordinates,
97    /// A list property inside `vertex`.
98    #[error("list properties inside the vertex element are not supported")]
99    ListInVertex,
100    /// The first element is not `vertex`.
101    #[error("the first PLY element must be vertex, found \"{0}\"")]
102    VertexNotFirst(String),
103    /// The data end earlier than the header promised.
104    #[error("truncated data: need {expected} bytes, {actual} available")]
105    Truncated {
106        /// How many bytes the header requires.
107        expected: usize,
108        /// How many bytes are present.
109        actual: usize,
110    },
111    /// A number could not be parsed.
112    #[error("could not parse the number \"{0}\"")]
113    BadNumber(String),
114    /// An error raised by the `las` crate.
115    #[error("LAS error: {0}")]
116    Las(String),
117    /// An error raised by the `e57` crate.
118    #[error("E57 error: {0}")]
119    E57(String),
120    /// The PCD header is malformed.
121    #[error("malformed PCD header: {0}")]
122    BadPcd(String),
123    /// A PCD field or layout this reader does not handle.
124    #[error("unsupported PCD data: \"{0}\"")]
125    UnsupportedPcd(String),
126    /// The extension names no format this crate knows.
127    ///
128    /// The readable ones are listed in [`READABLE`], the writable ones in
129    /// [`WRITABLE`]; the message names the extension rather than reciting
130    /// the list, because a file dialog offers the list already.
131    #[error("unknown format: \"{0}\"")]
132    UnknownFormat(String),
133    /// An error while building the cloud.
134    #[error(transparent)]
135    Cloud(#[from] rigidity_core::CloudError),
136}