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
12#[cfg(feature = "streaming")]
13use crate::streaming::{
14 read_bounded_ascii_line, records_io, FormatStreamState, MAX_ASCII_RECORD_BYTES,
15};
16#[cfg(feature = "streaming")]
17use spatialrust_records::{
18 BoundedSpatialRecordSource, CancellationToken, MemoryTracker, RecordsResult, SchemaDescriptor,
19 SpatialRecordChunk, StreamOptions,
20};
21
22pub struct PlyReader<R: BufRead> {
24 reader: R,
25 header: PlyHeader,
26 metadata: SpatialMetadata,
27 schema: PointSchema,
28 loaded: bool,
29}
30
31impl<R: BufRead> PlyReader<R> {
32 pub fn new(mut reader: R) -> Result<Self, IoError> {
34 let header = PlyHeader::parse(&mut reader)?;
35 let schema = schema_from_ply_properties(&header.properties)?;
36 let metadata = metadata_from_header(&header);
37 Ok(Self { reader, header, metadata, schema, loaded: false })
38 }
39
40 #[must_use]
42 pub fn header(&self) -> &PlyHeader {
43 &self.header
44 }
45
46 pub fn read_cloud(&mut self) -> Result<PointCloud, IoError> {
48 if self.loaded {
49 return Err(ply_format("PLY reader already consumed"));
50 }
51 self.loaded = true;
52 read_ply_body(&self.header, &mut self.reader, self.schema.clone(), self.metadata.clone())
53 }
54}
55
56impl<R: BufRead> PointReader for PlyReader<R> {
57 fn schema(&self) -> spatialrust_core::SpatialResult<PointSchema> {
58 Ok(self.schema.clone())
59 }
60
61 fn metadata(&self) -> spatialrust_core::SpatialResult<SpatialMetadata> {
62 Ok(self.metadata.clone())
63 }
64
65 fn read(&mut self, _options: &ReadOptions) -> spatialrust_core::SpatialResult<PointCloud> {
66 self.read_cloud().map_err(|error| spatialrust_core::SpatialError::Io(error.to_string()))
67 }
68}
69
70pub fn read_ply<R: BufRead>(reader: &mut R) -> Result<PointCloud, IoError> {
72 let header = PlyHeader::parse(reader)?;
73 let schema = schema_from_ply_properties(&header.properties)?;
74 let metadata = metadata_from_header(&header);
75 read_ply_body(&header, reader, schema, metadata)
76}
77
78fn read_ply_body<R: BufRead>(
79 header: &PlyHeader,
80 reader: &mut R,
81 schema: PointSchema,
82 metadata: SpatialMetadata,
83) -> Result<PointCloud, IoError> {
84 let mut buffers = PointBufferSet::new();
85 for field in schema.fields() {
86 buffers.insert(
87 field.name.clone(),
88 PointBuffer::with_capacity(field.dtype, header.vertex_count),
89 );
90 }
91
92 match header.format {
93 PlyFormat::Ascii => read_ascii_vertices(reader, header, &schema, &mut buffers)?,
94 PlyFormat::BinaryLittleEndian => {
95 read_binary_vertices(reader, header, &schema, &mut buffers)?
96 }
97 }
98
99 PointCloud::try_from_parts(schema, buffers, metadata).map_err(IoError::from)
100}
101
102fn metadata_from_header(_header: &PlyHeader) -> SpatialMetadata {
103 SpatialMetadata {
104 frame_id: spatialrust_core::FrameId::new("ply"),
105 timestamp: spatialrust_core::Timestamp::from_nanos(0),
106 sensor_origin: None,
107 unit: "meter".to_owned(),
108 }
109}
110
111fn read_ascii_vertices<R: BufRead>(
112 reader: &mut R,
113 header: &PlyHeader,
114 schema: &PointSchema,
115 buffers: &mut PointBufferSet,
116) -> Result<(), IoError> {
117 for vertex_index in 0..header.vertex_count {
118 let line = read_ascii_vertex_line(reader, vertex_index, header.vertex_count)?;
119 let mut tokens = line.split_whitespace();
120 for property in &header.properties {
121 let token = tokens.next().ok_or_else(|| {
122 ply_parse(format!(
123 "missing token for property `{}` on vertex {vertex_index}",
124 property.name
125 ))
126 })?;
127 let value = token
128 .parse::<f64>()
129 .map_err(|_| ply_parse(format!("invalid ASCII value `{token}`")))?;
130 push_to_field(buffers, schema, &property.name, value as f32)?;
131 }
132 }
133 Ok(())
134}
135
136fn read_ascii_vertex_line<R: BufRead>(
137 reader: &mut R,
138 vertex_index: usize,
139 vertex_count: usize,
140) -> Result<String, IoError> {
141 loop {
142 let mut line = String::new();
143 let read = reader.read_line(&mut line)?;
144 if read == 0 {
145 return Err(ply_parse(format!(
146 "unexpected EOF after {vertex_index} of {vertex_count} ASCII vertices"
147 )));
148 }
149 let trimmed = line.trim();
150 if trimmed.is_empty() || trimmed.starts_with('#') {
151 continue;
152 }
153 return Ok(trimmed.to_owned());
154 }
155}
156
157fn read_binary_vertices<R: BufRead>(
158 reader: &mut R,
159 header: &PlyHeader,
160 schema: &PointSchema,
161 buffers: &mut PointBufferSet,
162) -> Result<(), IoError> {
163 let mut payload = vec![0_u8; header.vertex_stride() * header.vertex_count];
164 std::io::Read::read_exact(&mut *reader, &mut payload).map_err(IoError::from)?;
165
166 for vertex_index in 0..header.vertex_count {
167 let start = vertex_index * header.vertex_stride();
168 let mut offset = 0usize;
169 for property in &header.properties {
170 let size = property.kind.size_bytes();
171 let chunk = &payload[start + offset..start + offset + size];
172 offset += size;
173 let value = read_binary_scalar(property.kind, chunk)?;
174 push_to_field(buffers, schema, &property.name, value)?;
175 }
176 }
177 Ok(())
178}
179
180fn read_binary_scalar(kind: PlyPropertyKind, chunk: &[u8]) -> Result<f32, IoError> {
181 let value = match kind {
182 PlyPropertyKind::Char => i8::from_le_bytes(chunk.try_into().expect("char")) as f32,
183 PlyPropertyKind::UChar => f32::from(chunk[0]),
184 PlyPropertyKind::Short => i16::from_le_bytes(chunk.try_into().expect("short")) as f32,
185 PlyPropertyKind::UShort => f32::from(u16::from_le_bytes(chunk.try_into().expect("ushort"))),
186 PlyPropertyKind::Int => i32::from_le_bytes(chunk.try_into().expect("int")) as f32,
187 PlyPropertyKind::UInt => u32::from_le_bytes(chunk.try_into().expect("uint")) as f32,
188 PlyPropertyKind::Float => f32::from_le_bytes(chunk.try_into().expect("float")),
189 PlyPropertyKind::Double => f64::from_le_bytes(chunk.try_into().expect("double")) as f32,
190 };
191 Ok(value)
192}
193
194fn push_to_field(
195 buffers: &mut PointBufferSet,
196 schema: &PointSchema,
197 name: &str,
198 value: f32,
199) -> Result<(), IoError> {
200 let field = schema
201 .fields()
202 .iter()
203 .find(|field| field.name == name)
204 .ok_or_else(|| ply_format(format!("schema missing mapped field `{name}`")))?;
205
206 let buffer = buffers
207 .get_mut(name)
208 .ok_or_else(|| ply_format(format!("buffer missing for field `{name}`")))?;
209
210 match field.dtype {
211 DType::F32 | DType::F16 => buffer.push_f32(value).map_err(IoError::from),
212 DType::F64 => buffer.push_f64(f64::from(value)).map_err(IoError::from),
213 DType::U8 => buffer.push_u8(value.round() as u8).map_err(IoError::from),
214 DType::U16 => buffer.push_u16(value.round() as u16).map_err(IoError::from),
215 DType::I32 => buffer.push_i32(value.round() as i32).map_err(IoError::from),
216 DType::U32 => {
217 let PointBuffer::U32(values) = buffer else {
218 return Err(IoError::Core(spatialrust_core::SpatialError::UnsupportedDType(
219 field.dtype,
220 )));
221 };
222 values.push(value.round() as u32);
223 Ok(())
224 }
225 }
226}
227
228pub fn read_ply_file(path: impl AsRef<std::path::Path>) -> Result<PointCloud, IoError> {
230 let file = std::fs::File::open(path.as_ref())?;
231 let mut reader = std::io::BufReader::new(file);
232 read_ply(&mut reader)
233}
234
235#[cfg(feature = "streaming")]
237pub struct PlyChunkSource<R: BufRead> {
238 reader: R,
239 header: PlyHeader,
240 metadata: SpatialMetadata,
241 state: FormatStreamState,
242 loaded: usize,
243}
244
245#[cfg(feature = "streaming")]
246impl<R: BufRead> PlyChunkSource<R> {
247 pub fn new(
249 mut reader: R,
250 options: StreamOptions,
251 cancellation: CancellationToken,
252 ) -> Result<Self, IoError> {
253 let header = PlyHeader::parse(&mut reader)?;
254 let schema = schema_from_ply_properties(&header.properties)?;
255 let metadata = metadata_from_header(&header);
256 let state = FormatStreamState::new("ply", schema, options, cancellation)?;
257 Ok(Self { reader, header, metadata, state, loaded: 0 })
258 }
259
260 #[must_use]
262 pub fn header(&self) -> &PlyHeader {
263 &self.header
264 }
265}
266
267#[cfg(feature = "streaming")]
268impl PlyChunkSource<std::io::BufReader<std::fs::File>> {
269 pub fn open(
271 path: impl AsRef<std::path::Path>,
272 options: StreamOptions,
273 cancellation: CancellationToken,
274 ) -> Result<Self, IoError> {
275 Self::new(std::io::BufReader::new(std::fs::File::open(path)?), options, cancellation)
276 }
277}
278
279#[cfg(feature = "streaming")]
280impl<R: BufRead> BoundedSpatialRecordSource for PlyChunkSource<R> {
281 fn schema(&self) -> &SchemaDescriptor {
282 &self.state.schema
283 }
284
285 fn options(&self) -> &StreamOptions {
286 &self.state.options
287 }
288
289 fn memory_tracker(&self) -> &MemoryTracker {
290 &self.state.tracker
291 }
292
293 fn cancellation_token(&self) -> CancellationToken {
294 self.state.cancellation.clone()
295 }
296
297 fn max_chunk_bytes(&self) -> u64 {
298 self.state.max_chunk_bytes
299 }
300
301 fn next_chunk(&mut self) -> Option<RecordsResult<SpatialRecordChunk>> {
302 if self.loaded == self.header.vertex_count {
303 return None;
304 }
305 let count = (self.header.vertex_count - self.loaded).min(self.state.options.chunk_points());
306 let scratch = match self.header.format {
307 PlyFormat::Ascii => 0,
308 PlyFormat::BinaryLittleEndian => match self
309 .header
310 .vertex_stride()
311 .checked_mul(count)
312 .and_then(|bytes| u64::try_from(bytes).ok())
313 {
314 Some(bytes) => bytes,
315 None => {
316 return Some(Err(spatialrust_records::RecordsError::InvalidChunk(
317 "PLY binary chunk size overflow".into(),
318 )));
319 }
320 },
321 };
322 let reservation = match self.state.reserve_points_with_scratch(count, scratch) {
323 Ok(reservation) => reservation,
324 Err(error) => return Some(Err(error)),
325 };
326 let schema = self.state.schema.point_schema().clone();
327 let mut buffers = PointBufferSet::new();
328 for field in schema.fields() {
329 buffers.insert(field.name.clone(), PointBuffer::with_capacity(field.dtype, count));
330 }
331
332 let decoded = match self.header.format {
333 PlyFormat::Ascii => {
334 let mut result = Ok(());
335 let mut line_buffer = [0_u8; MAX_ASCII_RECORD_BYTES];
336 let mut decoded_count = 0;
337 while decoded_count < count {
338 let line =
339 match read_bounded_ascii_line(&mut self.reader, &mut line_buffer, "PLY") {
340 Ok(Some(line)) => line,
341 Ok(None) => {
342 return Some(Err(records_io(ply_parse(format!(
343 "unexpected EOF after {} of {} ASCII vertices",
344 self.loaded + decoded_count,
345 self.header.vertex_count
346 )))));
347 }
348 Err(error) => return Some(Err(records_io(error))),
349 };
350 let trimmed = line.trim();
351 if trimmed.is_empty() || trimmed.starts_with('#') {
352 continue;
353 }
354 let mut tokens = trimmed.split_whitespace();
355 for property in &self.header.properties {
356 let value = tokens
357 .next()
358 .ok_or_else(|| {
359 ply_parse(format!("missing property `{}`", property.name))
360 })
361 .and_then(|token| {
362 token.parse::<f64>().map_err(|_| {
363 ply_parse(format!("invalid ASCII value `{token}`"))
364 })
365 })
366 .and_then(|value| {
367 push_to_field(&mut buffers, &schema, &property.name, value as f32)
368 });
369 if let Err(error) = value {
370 result = Err(error);
371 break;
372 }
373 }
374 if result.is_err() {
375 break;
376 }
377 decoded_count += 1;
378 }
379 result
380 }
381 PlyFormat::BinaryLittleEndian => {
382 let stride = self.header.vertex_stride();
383 let mut payload = vec![0_u8; stride * count];
384 std::io::Read::read_exact(&mut self.reader, &mut payload)
385 .map_err(IoError::from)
386 .and_then(|()| {
387 payload.chunks_exact(stride).try_for_each(|vertex| {
388 let mut offset = 0;
389 for property in &self.header.properties {
390 let size = property.kind.size_bytes();
391 let value = read_binary_scalar(
392 property.kind,
393 &vertex[offset..offset + size],
394 )?;
395 push_to_field(&mut buffers, &schema, &property.name, value)?;
396 offset += size;
397 }
398 Ok::<_, IoError>(())
399 })
400 })
401 }
402 };
403 if let Err(error) = decoded {
404 return Some(Err(records_io(error)));
405 }
406 let cloud = match PointCloud::try_from_parts(schema, buffers, self.metadata.clone()) {
407 Ok(cloud) => cloud,
408 Err(error) => return Some(Err(error.into())),
409 };
410 self.loaded += count;
411 Some(self.state.lease(cloud, reservation))
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::read_ply;
418 use crate::ply::writer::{write_ply, PlyWriteFormat};
419 use spatialrust_core::{HasIntensity, HasPositions3, PointCloudBuilder, StandardSchemas};
420 use std::io::Cursor;
421
422 const SAMPLE_XYZ_ASCII: &str = "\
423ply
424format ascii 1.0
425element vertex 3
426property float x
427property float y
428property float z
429end_header
4300.0 0.0 0.0
4311.0 0.0 0.0
4320.0 1.0 0.0
433";
434
435 const SAMPLE_XYZI_ASCII: &str = "\
436ply
437format ascii 1.0
438element vertex 2
439property float x
440property float y
441property float z
442property float intensity
443end_header
4440.0 0.0 0.0 0.5
4451.0 0.0 0.0 0.8
446";
447
448 #[test]
449 fn reads_ascii_xyz() {
450 let mut reader = Cursor::new(SAMPLE_XYZ_ASCII.as_bytes());
451 let cloud = read_ply(&mut reader).unwrap();
452 assert_eq!(cloud.len(), 3);
453 let (x, y, z) = cloud.positions3().unwrap();
454 assert_eq!(x, &[0.0, 1.0, 0.0]);
455 assert_eq!(y, &[0.0, 0.0, 1.0]);
456 assert_eq!(z, &[0.0, 0.0, 0.0]);
457 }
458
459 #[test]
460 fn reads_ascii_xyzi() {
461 let mut reader = Cursor::new(SAMPLE_XYZI_ASCII.as_bytes());
462 let cloud = read_ply(&mut reader).unwrap();
463 assert_eq!(cloud.intensity().unwrap(), &[0.5, 0.8]);
464 }
465
466 #[test]
467 fn roundtrip_ascii_xyz() {
468 let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyz());
469 builder.push_point([0.0, 0.0, 0.0]).unwrap();
470 builder.push_point([1.0, 2.0, 3.0]).unwrap();
471 let cloud = builder.build().unwrap();
472
473 let mut buffer = Vec::new();
474 write_ply(&mut buffer, &cloud, PlyWriteFormat::Ascii).unwrap();
475
476 let mut reader = Cursor::new(buffer);
477 let loaded = read_ply(&mut reader).unwrap();
478 assert_eq!(loaded.len(), cloud.len());
479 let (x, y, z) = loaded.positions3().unwrap();
480 assert_eq!(x, cloud.field("x").unwrap().as_f32().unwrap());
481 assert_eq!(y, cloud.field("y").unwrap().as_f32().unwrap());
482 assert_eq!(z, cloud.field("z").unwrap().as_f32().unwrap());
483 }
484
485 #[test]
486 fn roundtrip_binary_xyz() {
487 let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyz());
488 builder.push_point([0.5, 1.5, 2.5]).unwrap();
489 let cloud = builder.build().unwrap();
490
491 let mut buffer = Vec::new();
492 write_ply(&mut buffer, &cloud, PlyWriteFormat::BinaryLittleEndian).unwrap();
493
494 let mut reader = Cursor::new(buffer);
495 let loaded = read_ply(&mut reader).unwrap();
496 let (x, _, _) = loaded.positions3().unwrap();
497 assert!((x[0] - 0.5).abs() < 1e-6);
498 }
499}