1use std::io::BufRead;
2
3use spatialrust_core::{
4 DType, PointBuffer, PointBufferSet, PointCloud, PointSchema, SpatialMetadata,
5};
6
7use crate::error::{ply_format, ply_parse, IoError};
8use crate::ply::header::{PlyFormat, PlyHeader, PlyPropertyKind};
9use crate::ply::schema::schema_from_ply_properties;
10use crate::{PointReader, ReadOptions};
11
12pub struct PlyReader<R: BufRead> {
14 reader: R,
15 header: PlyHeader,
16 metadata: SpatialMetadata,
17 schema: PointSchema,
18 loaded: bool,
19}
20
21impl<R: BufRead> PlyReader<R> {
22 pub fn new(mut reader: R) -> Result<Self, IoError> {
24 let header = PlyHeader::parse(&mut reader)?;
25 let schema = schema_from_ply_properties(&header.properties)?;
26 let metadata = metadata_from_header(&header);
27 Ok(Self { reader, header, metadata, schema, loaded: false })
28 }
29
30 #[must_use]
32 pub fn header(&self) -> &PlyHeader {
33 &self.header
34 }
35
36 pub fn read_cloud(&mut self) -> Result<PointCloud, IoError> {
38 if self.loaded {
39 return Err(ply_format("PLY reader already consumed"));
40 }
41 self.loaded = true;
42 read_ply_body(&self.header, &mut self.reader, self.schema.clone(), self.metadata.clone())
43 }
44}
45
46impl<R: BufRead> PointReader for PlyReader<R> {
47 fn schema(&self) -> spatialrust_core::SpatialResult<PointSchema> {
48 Ok(self.schema.clone())
49 }
50
51 fn metadata(&self) -> spatialrust_core::SpatialResult<SpatialMetadata> {
52 Ok(self.metadata.clone())
53 }
54
55 fn read(&mut self, _options: &ReadOptions) -> spatialrust_core::SpatialResult<PointCloud> {
56 self.read_cloud().map_err(|error| spatialrust_core::SpatialError::Io(error.to_string()))
57 }
58}
59
60pub fn read_ply<R: BufRead>(reader: &mut R) -> Result<PointCloud, IoError> {
62 let header = PlyHeader::parse(reader)?;
63 let schema = schema_from_ply_properties(&header.properties)?;
64 let metadata = metadata_from_header(&header);
65 read_ply_body(&header, reader, schema, metadata)
66}
67
68fn read_ply_body<R: BufRead>(
69 header: &PlyHeader,
70 reader: &mut R,
71 schema: PointSchema,
72 metadata: SpatialMetadata,
73) -> Result<PointCloud, IoError> {
74 let mut buffers = PointBufferSet::new();
75 for field in schema.fields() {
76 buffers.insert(
77 field.name.clone(),
78 PointBuffer::with_capacity(field.dtype, header.vertex_count),
79 );
80 }
81
82 match header.format {
83 PlyFormat::Ascii => read_ascii_vertices(reader, header, &schema, &mut buffers)?,
84 PlyFormat::BinaryLittleEndian => {
85 read_binary_vertices(reader, header, &schema, &mut buffers)?
86 }
87 }
88
89 PointCloud::try_from_parts(schema, buffers, metadata).map_err(IoError::from)
90}
91
92fn metadata_from_header(_header: &PlyHeader) -> SpatialMetadata {
93 SpatialMetadata {
94 frame_id: spatialrust_core::FrameId::new("ply"),
95 timestamp: spatialrust_core::Timestamp::from_nanos(0),
96 sensor_origin: None,
97 unit: "meter".to_owned(),
98 }
99}
100
101fn read_ascii_vertices<R: BufRead>(
102 reader: &mut R,
103 header: &PlyHeader,
104 schema: &PointSchema,
105 buffers: &mut PointBufferSet,
106) -> Result<(), IoError> {
107 for vertex_index in 0..header.vertex_count {
108 let line = read_ascii_vertex_line(reader, vertex_index, header.vertex_count)?;
109 let mut tokens = line.split_whitespace();
110 for property in &header.properties {
111 let token = tokens.next().ok_or_else(|| {
112 ply_parse(format!(
113 "missing token for property `{}` on vertex {vertex_index}",
114 property.name
115 ))
116 })?;
117 let value = token
118 .parse::<f64>()
119 .map_err(|_| ply_parse(format!("invalid ASCII value `{token}`")))?;
120 push_to_field(buffers, schema, &property.name, value as f32)?;
121 }
122 }
123 Ok(())
124}
125
126fn read_ascii_vertex_line<R: BufRead>(
127 reader: &mut R,
128 vertex_index: usize,
129 vertex_count: usize,
130) -> Result<String, IoError> {
131 loop {
132 let mut line = String::new();
133 let read = reader.read_line(&mut line)?;
134 if read == 0 {
135 return Err(ply_parse(format!(
136 "unexpected EOF after {vertex_index} of {vertex_count} ASCII vertices"
137 )));
138 }
139 let trimmed = line.trim();
140 if trimmed.is_empty() || trimmed.starts_with('#') {
141 continue;
142 }
143 return Ok(trimmed.to_owned());
144 }
145}
146
147fn read_binary_vertices<R: BufRead>(
148 reader: &mut R,
149 header: &PlyHeader,
150 schema: &PointSchema,
151 buffers: &mut PointBufferSet,
152) -> Result<(), IoError> {
153 let mut payload = vec![0_u8; header.vertex_stride() * header.vertex_count];
154 std::io::Read::read_exact(&mut *reader, &mut payload).map_err(IoError::from)?;
155
156 for vertex_index in 0..header.vertex_count {
157 let start = vertex_index * header.vertex_stride();
158 let mut offset = 0usize;
159 for property in &header.properties {
160 let size = property.kind.size_bytes();
161 let chunk = &payload[start + offset..start + offset + size];
162 offset += size;
163 let value = read_binary_scalar(property.kind, chunk)?;
164 push_to_field(buffers, schema, &property.name, value)?;
165 }
166 }
167 Ok(())
168}
169
170fn read_binary_scalar(kind: PlyPropertyKind, chunk: &[u8]) -> Result<f32, IoError> {
171 let value = match kind {
172 PlyPropertyKind::Char => i8::from_le_bytes(chunk.try_into().expect("char")) as f32,
173 PlyPropertyKind::UChar => f32::from(chunk[0]),
174 PlyPropertyKind::Short => i16::from_le_bytes(chunk.try_into().expect("short")) as f32,
175 PlyPropertyKind::UShort => f32::from(u16::from_le_bytes(chunk.try_into().expect("ushort"))),
176 PlyPropertyKind::Int => i32::from_le_bytes(chunk.try_into().expect("int")) as f32,
177 PlyPropertyKind::UInt => u32::from_le_bytes(chunk.try_into().expect("uint")) as f32,
178 PlyPropertyKind::Float => f32::from_le_bytes(chunk.try_into().expect("float")),
179 PlyPropertyKind::Double => f64::from_le_bytes(chunk.try_into().expect("double")) as f32,
180 };
181 Ok(value)
182}
183
184fn push_to_field(
185 buffers: &mut PointBufferSet,
186 schema: &PointSchema,
187 name: &str,
188 value: f32,
189) -> Result<(), IoError> {
190 let field = schema
191 .fields()
192 .iter()
193 .find(|field| field.name == name)
194 .ok_or_else(|| ply_format(format!("schema missing mapped field `{name}`")))?;
195
196 let buffer = buffers
197 .get_mut(name)
198 .ok_or_else(|| ply_format(format!("buffer missing for field `{name}`")))?;
199
200 match field.dtype {
201 DType::F32 | DType::F16 => buffer.push_f32(value).map_err(IoError::from),
202 DType::F64 => buffer.push_f64(f64::from(value)).map_err(IoError::from),
203 DType::U8 => buffer.push_u8(value.round() as u8).map_err(IoError::from),
204 DType::U16 => buffer.push_u16(value.round() as u16).map_err(IoError::from),
205 DType::I32 => buffer.push_i32(value.round() as i32).map_err(IoError::from),
206 DType::U32 => {
207 let PointBuffer::U32(values) = buffer else {
208 return Err(IoError::Core(spatialrust_core::SpatialError::UnsupportedDType(
209 field.dtype,
210 )));
211 };
212 values.push(value.round() as u32);
213 Ok(())
214 }
215 }
216}
217
218pub fn read_ply_file(path: impl AsRef<std::path::Path>) -> Result<PointCloud, IoError> {
220 let file = std::fs::File::open(path.as_ref())?;
221 let mut reader = std::io::BufReader::new(file);
222 read_ply(&mut reader)
223}
224
225#[cfg(test)]
226mod tests {
227 use super::read_ply;
228 use crate::ply::writer::{write_ply, PlyWriteFormat};
229 use spatialrust_core::{HasIntensity, HasPositions3, PointCloudBuilder, StandardSchemas};
230 use std::io::Cursor;
231
232 const SAMPLE_XYZ_ASCII: &str = "\
233ply
234format ascii 1.0
235element vertex 3
236property float x
237property float y
238property float z
239end_header
2400.0 0.0 0.0
2411.0 0.0 0.0
2420.0 1.0 0.0
243";
244
245 const SAMPLE_XYZI_ASCII: &str = "\
246ply
247format ascii 1.0
248element vertex 2
249property float x
250property float y
251property float z
252property float intensity
253end_header
2540.0 0.0 0.0 0.5
2551.0 0.0 0.0 0.8
256";
257
258 #[test]
259 fn reads_ascii_xyz() {
260 let mut reader = Cursor::new(SAMPLE_XYZ_ASCII.as_bytes());
261 let cloud = read_ply(&mut reader).unwrap();
262 assert_eq!(cloud.len(), 3);
263 let (x, y, z) = cloud.positions3().unwrap();
264 assert_eq!(x, &[0.0, 1.0, 0.0]);
265 assert_eq!(y, &[0.0, 0.0, 1.0]);
266 assert_eq!(z, &[0.0, 0.0, 0.0]);
267 }
268
269 #[test]
270 fn reads_ascii_xyzi() {
271 let mut reader = Cursor::new(SAMPLE_XYZI_ASCII.as_bytes());
272 let cloud = read_ply(&mut reader).unwrap();
273 assert_eq!(cloud.intensity().unwrap(), &[0.5, 0.8]);
274 }
275
276 #[test]
277 fn roundtrip_ascii_xyz() {
278 let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyz());
279 builder.push_point([0.0, 0.0, 0.0]).unwrap();
280 builder.push_point([1.0, 2.0, 3.0]).unwrap();
281 let cloud = builder.build().unwrap();
282
283 let mut buffer = Vec::new();
284 write_ply(&mut buffer, &cloud, PlyWriteFormat::Ascii).unwrap();
285
286 let mut reader = Cursor::new(buffer);
287 let loaded = read_ply(&mut reader).unwrap();
288 assert_eq!(loaded.len(), cloud.len());
289 let (x, y, z) = loaded.positions3().unwrap();
290 assert_eq!(x, cloud.field("x").unwrap().as_f32().unwrap());
291 assert_eq!(y, cloud.field("y").unwrap().as_f32().unwrap());
292 assert_eq!(z, cloud.field("z").unwrap().as_f32().unwrap());
293 }
294
295 #[test]
296 fn roundtrip_binary_xyz() {
297 let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyz());
298 builder.push_point([0.5, 1.5, 2.5]).unwrap();
299 let cloud = builder.build().unwrap();
300
301 let mut buffer = Vec::new();
302 write_ply(&mut buffer, &cloud, PlyWriteFormat::BinaryLittleEndian).unwrap();
303
304 let mut reader = Cursor::new(buffer);
305 let loaded = read_ply(&mut reader).unwrap();
306 let (x, _, _) = loaded.positions3().unwrap();
307 assert!((x[0] - 0.5).abs() < 1e-6);
308 }
309}