1use std::io::BufRead;
2
3use spatialrust_core::{
4 DType, PointBuffer, PointBufferSet, PointCloud, PointSchema, SpatialMetadata,
5};
6
7use crate::error::{pcd_format, pcd_parse, IoError};
8use crate::pcd::header::{read_binary_payload, PcdDataKind, PcdHeader};
9use crate::pcd::schema::schema_from_pcd_fields;
10use crate::{PointReader, ReadOptions};
11
12pub struct PcdReader<R: BufRead> {
14 reader: R,
15 header: PcdHeader,
16 metadata: SpatialMetadata,
17 schema: PointSchema,
18 loaded: bool,
19}
20
21impl<R: BufRead> PcdReader<R> {
22 pub fn new(mut reader: R) -> Result<Self, IoError> {
24 let (header, _) = PcdHeader::parse(&mut reader)?;
25 let schema = schema_from_pcd_fields(&header.fields)?;
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) -> &PcdHeader {
33 &self.header
34 }
35
36 pub fn read_cloud(&mut self) -> Result<PointCloud, IoError> {
38 if self.loaded {
39 return Err(pcd_format("PCD reader already consumed"));
40 }
41 self.loaded = true;
42 read_pcd_body(&self.header, &mut self.reader, self.schema.clone(), self.metadata.clone())
43 }
44}
45
46impl<R: BufRead> PointReader for PcdReader<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_pcd<R: BufRead>(reader: &mut R) -> Result<PointCloud, IoError> {
62 let (header, _) = PcdHeader::parse(reader)?;
63 let schema = schema_from_pcd_fields(&header.fields)?;
64 let metadata = metadata_from_header(&header);
65 read_pcd_body(&header, reader, schema, metadata)
66}
67
68fn read_pcd_body<R: BufRead>(
69 header: &PcdHeader,
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(field.name.clone(), PointBuffer::with_capacity(field.dtype, header.points));
77 }
78
79 match header.data {
80 PcdDataKind::Ascii => read_ascii_payload(reader, header, &schema, &mut buffers)?,
81 PcdDataKind::Binary => {
82 let payload = read_binary_payload(reader, header.point_step() * header.points)?;
83 decode_binary_payload(header, &schema, &payload, &mut buffers)?;
84 }
85 PcdDataKind::BinaryCompressed => {
86 let payload = read_binary_compressed_payload(reader)?;
87 decode_binary_compressed_payload(header, &schema, &payload, &mut buffers)?;
88 }
89 }
90
91 PointCloud::try_from_parts(schema, buffers, metadata).map_err(IoError::from)
92}
93
94fn metadata_from_header(_header: &PcdHeader) -> SpatialMetadata {
95 SpatialMetadata {
96 frame_id: spatialrust_core::FrameId::new("pcd"),
97 timestamp: spatialrust_core::Timestamp::from_nanos(0),
98 sensor_origin: None,
99 unit: "meter".to_owned(),
100 }
101}
102
103fn read_ascii_payload<R: BufRead>(
104 reader: &mut R,
105 header: &PcdHeader,
106 schema: &PointSchema,
107 buffers: &mut PointBufferSet,
108) -> Result<(), IoError> {
109 let mut loaded = 0usize;
110 while loaded < header.points {
111 let mut line = String::new();
112 let read = reader.read_line(&mut line)?;
113 if read == 0 {
114 return Err(pcd_parse(format!(
115 "unexpected EOF after {loaded} of {} ASCII points",
116 header.points
117 )));
118 }
119 let trimmed = line.trim();
120 if trimmed.is_empty() || trimmed.starts_with('#') {
121 continue;
122 }
123
124 let mut tokens = trimmed.split_whitespace();
125 for field in &header.fields {
126 if field.name.eq_ignore_ascii_case("rgb") {
127 let token =
128 tokens.next().ok_or_else(|| pcd_parse("missing rgb token in ASCII PCD"))?;
129 let packed = parse_packed_rgb(token)?;
130 push_to_field(buffers, schema, "r", packed.0)?;
131 push_to_field(buffers, schema, "g", packed.1)?;
132 push_to_field(buffers, schema, "b", packed.2)?;
133 continue;
134 }
135
136 for _ in 0..field.count {
137 let token = tokens.next().ok_or_else(|| {
138 pcd_parse(format!("missing token for field `{}`", field.name))
139 })?;
140 let value = token
141 .parse::<f32>()
142 .map_err(|_| pcd_parse(format!("invalid ASCII value `{token}`")))?;
143 push_to_field(buffers, schema, &field.name, value)?;
144 }
145 }
146 loaded += 1;
147 }
148 Ok(())
149}
150
151fn parse_packed_rgb(token: &str) -> Result<(f32, f32, f32), IoError> {
152 let float_value: f32 =
153 token.parse().map_err(|_| pcd_parse(format!("invalid rgb value `{token}`")))?;
154 let bits = float_value.to_bits();
155 Ok((((bits >> 16) & 0xFF) as f32, ((bits >> 8) & 0xFF) as f32, (bits & 0xFF) as f32))
156}
157
158fn read_binary_compressed_payload<R: BufRead>(reader: &mut R) -> Result<Vec<u8>, IoError> {
159 let mut size_buf = [0_u8; 4];
160 reader.read_exact(&mut size_buf)?;
161 let compressed_size = u32::from_le_bytes(size_buf) as usize;
162 reader.read_exact(&mut size_buf)?;
163 let uncompressed_size = u32::from_le_bytes(size_buf) as usize;
164
165 let compressed = read_binary_payload(reader, compressed_size)?;
166 lzf_decompress(&compressed, uncompressed_size)
167}
168
169fn lzf_decompress(input: &[u8], output_len: usize) -> Result<Vec<u8>, IoError> {
170 let mut output = vec![0_u8; output_len];
171 let mut ip = 0usize;
172 let mut op = 0usize;
173
174 while ip < input.len() {
175 let ctrl = input[ip];
176 ip += 1;
177
178 if ctrl < 32 {
179 let len = ctrl as usize + 1;
180 if ip + len > input.len() || op + len > output.len() {
181 return Err(pcd_format("truncated LZF literal run in binary_compressed PCD"));
182 }
183 output[op..op + len].copy_from_slice(&input[ip..ip + len]);
184 ip += len;
185 op += len;
186 continue;
187 }
188
189 let mut len = (ctrl >> 5) as usize;
190 let mut reference_offset = ((ctrl as usize & 0x1f) << 8) + 1;
191 if len == 7 {
192 if ip >= input.len() {
193 return Err(pcd_format("truncated LZF length in binary_compressed PCD"));
194 }
195 len += input[ip] as usize;
196 ip += 1;
197 }
198 if ip >= input.len() {
199 return Err(pcd_format("truncated LZF back-reference in binary_compressed PCD"));
200 }
201 reference_offset += input[ip] as usize;
202 ip += 1;
203
204 let copy_len = len + 2;
205 if reference_offset > op || op + copy_len > output.len() {
206 return Err(pcd_format("invalid LZF back-reference in binary_compressed PCD"));
207 }
208 let ref_start = op - reference_offset;
209 for offset in 0..copy_len {
210 output[op + offset] = output[ref_start + offset];
211 }
212 op += copy_len;
213 }
214
215 if op != output.len() {
216 return Err(pcd_format(format!(
217 "LZF payload size mismatch: expected {}, decoded {op}",
218 output.len()
219 )));
220 }
221 Ok(output)
222}
223
224fn decode_binary_payload(
225 header: &PcdHeader,
226 schema: &PointSchema,
227 payload: &[u8],
228 buffers: &mut PointBufferSet,
229) -> Result<(), IoError> {
230 let point_step = header.point_step();
231 if payload.len() != point_step * header.points {
232 return Err(pcd_format(format!(
233 "binary payload size mismatch: expected {}, found {}",
234 point_step * header.points,
235 payload.len()
236 )));
237 }
238
239 for point_index in 0..header.points {
240 let start = point_index * point_step;
241 let end = start + point_step;
242 decode_binary_point(&header.fields, &payload[start..end], schema, buffers)?;
243 }
244 Ok(())
245}
246
247fn decode_binary_compressed_payload(
248 header: &PcdHeader,
249 schema: &PointSchema,
250 payload: &[u8],
251 buffers: &mut PointBufferSet,
252) -> Result<(), IoError> {
253 let point_step = header.point_step();
254 if payload.len() != point_step * header.points {
255 return Err(pcd_format(format!(
256 "binary_compressed payload size mismatch: expected {}, found {}",
257 point_step * header.points,
258 payload.len()
259 )));
260 }
261
262 let mut field_base = 0usize;
263 for field in &header.fields {
264 let field_step = field.byte_size();
265 for point_index in 0..header.points {
266 let point_base = field_base + point_index * field_step;
267 if field.name.eq_ignore_ascii_case("rgb") && field.count == 1 && field.size == 4 {
268 let chunk = &payload[point_base..point_base + 4];
269 let bits = u32::from_le_bytes(chunk.try_into().expect("rgb chunk"));
270 push_to_field(buffers, schema, "r", ((bits >> 16) & 0xFF) as f32)?;
271 push_to_field(buffers, schema, "g", ((bits >> 8) & 0xFF) as f32)?;
272 push_to_field(buffers, schema, "b", (bits & 0xFF) as f32)?;
273 continue;
274 }
275
276 for component in 0..field.count {
277 let scalar_start = point_base + component * field.size;
278 let scalar_end = scalar_start + field.size;
279 let value = read_binary_scalar(field, &payload[scalar_start..scalar_end])?;
280 push_to_field(buffers, schema, &field.name, value)?;
281 }
282 }
283 field_base += field_step * header.points;
284 }
285 Ok(())
286}
287
288fn decode_binary_point(
289 fields: &[crate::pcd::schema::PcdFieldSpec],
290 bytes: &[u8],
291 schema: &PointSchema,
292 buffers: &mut PointBufferSet,
293) -> Result<(), IoError> {
294 let mut offset = 0usize;
295 for field in fields {
296 let size = field.byte_size();
297 if offset + size > bytes.len() {
298 return Err(pcd_parse("truncated binary PCD point"));
299 }
300 let field_start = offset;
301 offset += size;
302
303 if field.name.eq_ignore_ascii_case("rgb") && field.count == 1 && field.size == 4 {
304 let chunk = &bytes[field_start..field_start + 4];
305 let bits = u32::from_le_bytes(chunk.try_into().expect("rgb chunk"));
306 push_to_field(buffers, schema, "r", ((bits >> 16) & 0xFF) as f32)?;
307 push_to_field(buffers, schema, "g", ((bits >> 8) & 0xFF) as f32)?;
308 push_to_field(buffers, schema, "b", (bits & 0xFF) as f32)?;
309 continue;
310 }
311
312 for component in 0..field.count {
313 let scalar_start = field_start + component * field.size;
314 let scalar_end = scalar_start + field.size;
315 let value = read_binary_scalar(field, &bytes[scalar_start..scalar_end])?;
316 push_to_field(buffers, schema, &field.name, value)?;
317 }
318 }
319 Ok(())
320}
321
322fn read_binary_scalar(
323 field: &crate::pcd::schema::PcdFieldSpec,
324 chunk: &[u8],
325) -> Result<f32, IoError> {
326 let value = match (field.kind, field.size) {
327 (crate::pcd::schema::PcdType::F, 4) => f32::from_le_bytes(chunk.try_into().expect("f32")),
328 (crate::pcd::schema::PcdType::F, 8) => {
329 f64::from_le_bytes(chunk.try_into().expect("f64")) as f32
330 }
331 (crate::pcd::schema::PcdType::I, 4) => {
332 i32::from_le_bytes(chunk.try_into().expect("i32")) as f32
333 }
334 (crate::pcd::schema::PcdType::U, 1) => f32::from(chunk[0]),
335 (crate::pcd::schema::PcdType::U, 2) => {
336 f32::from(u16::from_le_bytes(chunk.try_into().expect("u16")))
337 }
338 (crate::pcd::schema::PcdType::U, 4) => {
339 u32::from_le_bytes(chunk.try_into().expect("u32")) as f32
340 }
341 _ => return Err(pcd_format(format!("unsupported binary field `{}`", field.name))),
342 };
343 Ok(value)
344}
345
346fn push_to_field(
347 buffers: &mut PointBufferSet,
348 schema: &PointSchema,
349 name: &str,
350 value: f32,
351) -> Result<(), IoError> {
352 let field = schema
353 .fields()
354 .iter()
355 .find(|field| field.name == name)
356 .ok_or_else(|| pcd_format(format!("schema missing mapped field `{name}`")))?;
357
358 let buffer = buffers
359 .get_mut(name)
360 .ok_or_else(|| pcd_format(format!("buffer missing for field `{name}`")))?;
361
362 match field.dtype {
363 DType::F32 | DType::F16 => buffer.push_f32(value).map_err(IoError::from),
364 DType::F64 => buffer.push_f64(f64::from(value)).map_err(IoError::from),
365 DType::U8 => buffer.push_u8(value.round() as u8).map_err(IoError::from),
366 DType::U16 => buffer.push_u16(value.round() as u16).map_err(IoError::from),
367 DType::I32 => buffer.push_i32(value.round() as i32).map_err(IoError::from),
368 DType::U32 => {
369 let PointBuffer::U32(values) = buffer else {
370 return Err(IoError::Core(spatialrust_core::SpatialError::UnsupportedDType(
371 field.dtype,
372 )));
373 };
374 values.push(value.round() as u32);
375 Ok(())
376 }
377 }
378}
379
380pub fn read_pcd_file(path: impl AsRef<std::path::Path>) -> Result<PointCloud, IoError> {
382 let file = std::fs::File::open(path.as_ref())?;
383 let mut reader = std::io::BufReader::new(file);
384 read_pcd(&mut reader)
385}
386
387#[cfg(test)]
388mod tests {
389 use super::read_pcd;
390 use crate::pcd::writer::{write_pcd, PcdWriteFormat};
391 use spatialrust_core::{HasIntensity, HasPositions3, PointCloudBuilder, StandardSchemas};
392 use std::io::Cursor;
393
394 const SAMPLE_XYZ_ASCII: &str = "\
395# .PCD v0.7 - Point Cloud Data file format
396VERSION 0.7
397FIELDS x y z
398SIZE 4 4 4
399TYPE F F F
400COUNT 1 1 1
401WIDTH 3
402HEIGHT 1
403VIEWPOINT 0 0 0 1 0 0 0
404POINTS 3
405DATA ascii
4060.0 0.0 0.0
4071.0 0.0 0.0
4080.0 1.0 0.0
409";
410
411 const SAMPLE_XYZI_ASCII: &str = "\
412VERSION 0.7
413FIELDS x y z intensity
414SIZE 4 4 4 4
415TYPE F F F F
416COUNT 1 1 1 1
417WIDTH 2
418HEIGHT 1
419VIEWPOINT 0 0 0 1 0 0 0
420POINTS 2
421DATA ascii
4220.0 0.0 0.0 0.5
4231.0 0.0 0.0 0.8
424";
425
426 fn binary_compressed_xyz_sample() -> Vec<u8> {
427 let header = b"\
428# .PCD v0.7 - Point Cloud Data file format
429VERSION 0.7
430FIELDS x y z
431SIZE 4 4 4
432TYPE F F F
433COUNT 1 1 1
434WIDTH 2
435HEIGHT 1
436VIEWPOINT 0 0 0 1 0 0 0
437POINTS 2
438DATA binary_compressed
439";
440 let mut uncompressed = Vec::new();
441 for value in [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0] {
444 uncompressed.extend_from_slice(&value.to_le_bytes());
445 }
446
447 let mut compressed = Vec::with_capacity(uncompressed.len() + 1);
448 compressed.push((uncompressed.len() - 1) as u8);
449 compressed.extend_from_slice(&uncompressed);
450
451 let mut data = header.to_vec();
452 data.extend_from_slice(&(compressed.len() as u32).to_le_bytes());
453 data.extend_from_slice(&(uncompressed.len() as u32).to_le_bytes());
454 data.extend_from_slice(&compressed);
455 data
456 }
457
458 #[test]
459 fn reads_ascii_xyz() {
460 let mut reader = Cursor::new(SAMPLE_XYZ_ASCII.as_bytes());
461 let cloud = read_pcd(&mut reader).unwrap();
462 assert_eq!(cloud.len(), 3);
463 let (x, y, z) = cloud.positions3().unwrap();
464 assert_eq!(x, &[0.0, 1.0, 0.0]);
465 assert_eq!(y, &[0.0, 0.0, 1.0]);
466 assert_eq!(z, &[0.0, 0.0, 0.0]);
467 }
468
469 #[test]
470 fn reads_ascii_xyzi() {
471 let mut reader = Cursor::new(SAMPLE_XYZI_ASCII.as_bytes());
472 let cloud = read_pcd(&mut reader).unwrap();
473 assert_eq!(cloud.intensity().unwrap(), &[0.5, 0.8]);
474 }
475
476 #[test]
477 fn roundtrip_ascii_xyz() {
478 let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyz());
479 builder.push_point([0.0, 0.0, 0.0]).unwrap();
480 builder.push_point([1.0, 2.0, 3.0]).unwrap();
481 let cloud = builder.build().unwrap();
482
483 let mut buffer = Vec::new();
484 write_pcd(&mut buffer, &cloud, PcdWriteFormat::Ascii).unwrap();
485
486 let mut reader = Cursor::new(buffer);
487 let loaded = read_pcd(&mut reader).unwrap();
488 assert_eq!(loaded.len(), cloud.len());
489 let (x, y, z) = loaded.positions3().unwrap();
490 assert_eq!(x, cloud.field("x").unwrap().as_f32().unwrap());
491 assert_eq!(y, cloud.field("y").unwrap().as_f32().unwrap());
492 assert_eq!(z, cloud.field("z").unwrap().as_f32().unwrap());
493 }
494
495 #[test]
496 fn roundtrip_binary_xyz() {
497 let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyz());
498 builder.push_point([0.5, 1.5, 2.5]).unwrap();
499 let cloud = builder.build().unwrap();
500
501 let mut buffer = Vec::new();
502 write_pcd(&mut buffer, &cloud, PcdWriteFormat::Binary).unwrap();
503
504 let mut reader = Cursor::new(buffer);
505 let loaded = read_pcd(&mut reader).unwrap();
506 let (x, _, _) = loaded.positions3().unwrap();
507 assert!((x[0] - 0.5).abs() < 1e-6);
508 }
509
510 #[test]
511 fn reads_binary_compressed_xyz() {
512 let data = binary_compressed_xyz_sample();
513 let mut reader = Cursor::new(data);
514 let loaded = read_pcd(&mut reader).unwrap();
515 let (x, y, z) = loaded.positions3().unwrap();
516 assert_eq!(x, &[1.0, 2.0]);
517 assert_eq!(y, &[3.0, 4.0]);
518 assert_eq!(z, &[5.0, 6.0]);
519 }
520}