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;
17
18pub use csv::{read_csv, read_poses};
19pub use e57_format::{read_e57, write_e57};
20pub use las_format::{read_las, write_las};
21pub use pcd::{read_pcd, write_pcd};
22pub use ply::{read_ply, write_ply};
23
24use std::path::Path;
25
26use rigidity_core::PointCloud;
27
28/// Reads a cloud, choosing the format by the file's extension.
29///
30/// The extension is all there is to go on and all anyone uses. A reader
31/// that sniffed the contents would be right more often and would also
32/// silently open a file the person did not mean to open.
33pub fn read(path: &Path) -> Result<PointCloud, IoError> {
34    match extension(path).as_str() {
35        "ply" => read_ply(path),
36        "las" | "laz" => read_las(path),
37        "e57" => read_e57(path),
38        "pcd" => read_pcd(path),
39        "csv" | "txt" => read_csv(path),
40        other => Err(IoError::UnknownFormat(other.to_owned())),
41    }
42}
43
44/// Writes a cloud, choosing the format by the file's extension.
45pub fn write(cloud: &PointCloud, path: &Path) -> Result<(), IoError> {
46    match extension(path).as_str() {
47        "ply" => write_ply(cloud, path),
48        "las" | "laz" => write_las(cloud, path),
49        "e57" => write_e57(cloud, path),
50        "pcd" => write_pcd(cloud, path),
51        other => Err(IoError::UnknownFormat(other.to_owned())),
52    }
53}
54
55/// Every extension `read` understands, for a file dialog to offer.
56pub const READABLE: &[&str] = &["ply", "las", "laz", "e57", "pcd", "csv", "txt"];
57
58/// Every extension `write` understands.
59pub const WRITABLE: &[&str] = &["ply", "las", "laz", "e57", "pcd"];
60
61fn extension(path: &Path) -> String {
62    path.extension()
63        .and_then(|extension| extension.to_str())
64        .unwrap_or_default()
65        .to_ascii_lowercase()
66}
67
68/// Read and write errors.
69#[derive(Debug, thiserror::Error)]
70pub enum IoError {
71    /// A filesystem error.
72    #[error("I/O error: {0}")]
73    Io(#[from] std::io::Error),
74    /// The file does not begin with the `ply` signature.
75    #[error("not a PLY file: the first line is not \"ply\"")]
76    NotPly,
77    /// Unsupported PLY format.
78    #[error("PLY format \"{0}\" is not supported: need ascii or binary_little_endian")]
79    UnsupportedFormat(String),
80    /// The header is malformed.
81    #[error("malformed PLY header: {0}")]
82    BadHeader(String),
83    /// Unknown property type.
84    #[error("unknown PLY property type: \"{0}\"")]
85    UnknownPropertyType(String),
86    /// The `vertex` element carries no coordinates.
87    #[error("the vertex element is missing the x, y, z properties")]
88    MissingCoordinates,
89    /// A list property inside `vertex`.
90    #[error("list properties inside the vertex element are not supported")]
91    ListInVertex,
92    /// The first element is not `vertex`.
93    #[error("the first PLY element must be vertex, found \"{0}\"")]
94    VertexNotFirst(String),
95    /// The data end earlier than the header promised.
96    #[error("truncated data: need {expected} bytes, {actual} available")]
97    Truncated {
98        /// How many bytes the header requires.
99        expected: usize,
100        /// How many bytes are present.
101        actual: usize,
102    },
103    /// A number could not be parsed.
104    #[error("could not parse the number \"{0}\"")]
105    BadNumber(String),
106    /// An error raised by the `las` crate.
107    #[error("LAS error: {0}")]
108    Las(String),
109    /// An error raised by the `e57` crate.
110    #[error("E57 error: {0}")]
111    E57(String),
112    /// The PCD header is malformed.
113    #[error("malformed PCD header: {0}")]
114    BadPcd(String),
115    /// A PCD field or layout this reader does not handle.
116    #[error("unsupported PCD data: \"{0}\"")]
117    UnsupportedPcd(String),
118    /// The extension names no format this crate knows.
119    ///
120    /// The readable ones are listed in [`READABLE`], the writable ones in
121    /// [`WRITABLE`]; the message names the extension rather than reciting
122    /// the list, because a file dialog offers the list already.
123    #[error("unknown format: \"{0}\"")]
124    UnknownFormat(String),
125    /// An error while building the cloud.
126    #[error(transparent)]
127    Cloud(#[from] rigidity_core::CloudError),
128}