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
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 PcdReader<R: BufRead> {
24 reader: R,
25 header: PcdHeader,
26 metadata: SpatialMetadata,
27 schema: PointSchema,
28 loaded: bool,
29}
30
31impl<R: BufRead> PcdReader<R> {
32 pub fn new(mut reader: R) -> Result<Self, IoError> {
34 let (header, _) = PcdHeader::parse(&mut reader)?;
35 let schema = schema_from_pcd_fields(&header.fields)?;
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) -> &PcdHeader {
43 &self.header
44 }
45
46 pub fn read_cloud(&mut self) -> Result<PointCloud, IoError> {
48 if self.loaded {
49 return Err(pcd_format("PCD reader already consumed"));
50 }
51 self.loaded = true;
52 read_pcd_body(&self.header, &mut self.reader, self.schema.clone(), self.metadata.clone())
53 }
54}
55
56impl<R: BufRead> PointReader for PcdReader<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_pcd<R: BufRead>(reader: &mut R) -> Result<PointCloud, IoError> {
72 let (header, _) = PcdHeader::parse(reader)?;
73 let schema = schema_from_pcd_fields(&header.fields)?;
74 let metadata = metadata_from_header(&header);
75 read_pcd_body(&header, reader, schema, metadata)
76}
77
78fn read_pcd_body<R: BufRead>(
79 header: &PcdHeader,
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(field.name.clone(), PointBuffer::with_capacity(field.dtype, header.points));
87 }
88
89 match header.data {
90 PcdDataKind::Ascii => read_ascii_payload(reader, header, &schema, &mut buffers)?,
91 PcdDataKind::Binary => {
92 let payload = read_binary_payload(reader, header.point_step() * header.points)?;
93 decode_binary_payload(header, &schema, &payload, &mut buffers)?;
94 }
95 PcdDataKind::BinaryCompressed => {
96 let payload = read_binary_compressed_payload(reader)?;
97 decode_binary_compressed_payload(header, &schema, &payload, &mut buffers)?;
98 }
99 }
100
101 PointCloud::try_from_parts(schema, buffers, metadata).map_err(IoError::from)
102}
103
104fn metadata_from_header(_header: &PcdHeader) -> SpatialMetadata {
105 SpatialMetadata {
106 frame_id: spatialrust_core::FrameId::new("pcd"),
107 timestamp: spatialrust_core::Timestamp::from_nanos(0),
108 sensor_origin: None,
109 unit: "meter".to_owned(),
110 }
111}
112
113fn read_ascii_payload<R: BufRead>(
114 reader: &mut R,
115 header: &PcdHeader,
116 schema: &PointSchema,
117 buffers: &mut PointBufferSet,
118) -> Result<(), IoError> {
119 let mut loaded = 0usize;
120 while loaded < header.points {
121 let mut line = String::new();
122 let read = reader.read_line(&mut line)?;
123 if read == 0 {
124 return Err(pcd_parse(format!(
125 "unexpected EOF after {loaded} of {} ASCII points",
126 header.points
127 )));
128 }
129 let trimmed = line.trim();
130 if trimmed.is_empty() || trimmed.starts_with('#') {
131 continue;
132 }
133
134 let mut tokens = trimmed.split_whitespace();
135 for field in &header.fields {
136 if field.name.eq_ignore_ascii_case("rgb") {
137 let token =
138 tokens.next().ok_or_else(|| pcd_parse("missing rgb token in ASCII PCD"))?;
139 let packed = parse_packed_rgb(token)?;
140 push_to_field(buffers, schema, "r", packed.0)?;
141 push_to_field(buffers, schema, "g", packed.1)?;
142 push_to_field(buffers, schema, "b", packed.2)?;
143 continue;
144 }
145
146 for _ in 0..field.count {
147 let token = tokens.next().ok_or_else(|| {
148 pcd_parse(format!("missing token for field `{}`", field.name))
149 })?;
150 let value = token
151 .parse::<f32>()
152 .map_err(|_| pcd_parse(format!("invalid ASCII value `{token}`")))?;
153 push_to_field(buffers, schema, &field.name, value)?;
154 }
155 }
156 loaded += 1;
157 }
158 Ok(())
159}
160
161fn parse_packed_rgb(token: &str) -> Result<(f32, f32, f32), IoError> {
162 let float_value: f32 =
163 token.parse().map_err(|_| pcd_parse(format!("invalid rgb value `{token}`")))?;
164 let bits = float_value.to_bits();
165 Ok((((bits >> 16) & 0xFF) as f32, ((bits >> 8) & 0xFF) as f32, (bits & 0xFF) as f32))
166}
167
168fn read_binary_compressed_payload<R: BufRead>(reader: &mut R) -> Result<Vec<u8>, IoError> {
169 let mut size_buf = [0_u8; 4];
170 reader.read_exact(&mut size_buf)?;
171 let compressed_size = u32::from_le_bytes(size_buf) as usize;
172 reader.read_exact(&mut size_buf)?;
173 let uncompressed_size = u32::from_le_bytes(size_buf) as usize;
174
175 let compressed = read_binary_payload(reader, compressed_size)?;
176 lzf_decompress(&compressed, uncompressed_size)
177}
178
179fn lzf_decompress(input: &[u8], output_len: usize) -> Result<Vec<u8>, IoError> {
180 let mut output = vec![0_u8; output_len];
181 let mut ip = 0usize;
182 let mut op = 0usize;
183
184 while ip < input.len() {
185 let ctrl = input[ip];
186 ip += 1;
187
188 if ctrl < 32 {
189 let len = ctrl as usize + 1;
190 if ip + len > input.len() || op + len > output.len() {
191 return Err(pcd_format("truncated LZF literal run in binary_compressed PCD"));
192 }
193 output[op..op + len].copy_from_slice(&input[ip..ip + len]);
194 ip += len;
195 op += len;
196 continue;
197 }
198
199 let mut len = (ctrl >> 5) as usize;
200 let mut reference_offset = ((ctrl as usize & 0x1f) << 8) + 1;
201 if len == 7 {
202 if ip >= input.len() {
203 return Err(pcd_format("truncated LZF length in binary_compressed PCD"));
204 }
205 len += input[ip] as usize;
206 ip += 1;
207 }
208 if ip >= input.len() {
209 return Err(pcd_format("truncated LZF back-reference in binary_compressed PCD"));
210 }
211 reference_offset += input[ip] as usize;
212 ip += 1;
213
214 let copy_len = len + 2;
215 if reference_offset > op || op + copy_len > output.len() {
216 return Err(pcd_format("invalid LZF back-reference in binary_compressed PCD"));
217 }
218 let ref_start = op - reference_offset;
219 for offset in 0..copy_len {
220 output[op + offset] = output[ref_start + offset];
221 }
222 op += copy_len;
223 }
224
225 if op != output.len() {
226 return Err(pcd_format(format!(
227 "LZF payload size mismatch: expected {}, decoded {op}",
228 output.len()
229 )));
230 }
231 Ok(output)
232}
233
234fn decode_binary_payload(
235 header: &PcdHeader,
236 schema: &PointSchema,
237 payload: &[u8],
238 buffers: &mut PointBufferSet,
239) -> Result<(), IoError> {
240 let point_step = header.point_step();
241 if payload.len() != point_step * header.points {
242 return Err(pcd_format(format!(
243 "binary payload size mismatch: expected {}, found {}",
244 point_step * header.points,
245 payload.len()
246 )));
247 }
248
249 for point_index in 0..header.points {
250 let start = point_index * point_step;
251 let end = start + point_step;
252 decode_binary_point(&header.fields, &payload[start..end], schema, buffers)?;
253 }
254 Ok(())
255}
256
257fn decode_binary_compressed_payload(
258 header: &PcdHeader,
259 schema: &PointSchema,
260 payload: &[u8],
261 buffers: &mut PointBufferSet,
262) -> Result<(), IoError> {
263 let point_step = header.point_step();
264 if payload.len() != point_step * header.points {
265 return Err(pcd_format(format!(
266 "binary_compressed payload size mismatch: expected {}, found {}",
267 point_step * header.points,
268 payload.len()
269 )));
270 }
271
272 let mut field_base = 0usize;
273 for field in &header.fields {
274 let field_step = field.byte_size();
275 for point_index in 0..header.points {
276 let point_base = field_base + point_index * field_step;
277 if field.name.eq_ignore_ascii_case("rgb") && field.count == 1 && field.size == 4 {
278 let chunk = &payload[point_base..point_base + 4];
279 let bits = u32::from_le_bytes(chunk.try_into().expect("rgb chunk"));
280 push_to_field(buffers, schema, "r", ((bits >> 16) & 0xFF) as f32)?;
281 push_to_field(buffers, schema, "g", ((bits >> 8) & 0xFF) as f32)?;
282 push_to_field(buffers, schema, "b", (bits & 0xFF) as f32)?;
283 continue;
284 }
285
286 for component in 0..field.count {
287 let scalar_start = point_base + component * field.size;
288 let scalar_end = scalar_start + field.size;
289 let value = read_binary_scalar(field, &payload[scalar_start..scalar_end])?;
290 push_to_field(buffers, schema, &field.name, value)?;
291 }
292 }
293 field_base += field_step * header.points;
294 }
295 Ok(())
296}
297
298fn decode_binary_point(
299 fields: &[crate::pcd::schema::PcdFieldSpec],
300 bytes: &[u8],
301 schema: &PointSchema,
302 buffers: &mut PointBufferSet,
303) -> Result<(), IoError> {
304 let mut offset = 0usize;
305 for field in fields {
306 let size = field.byte_size();
307 if offset + size > bytes.len() {
308 return Err(pcd_parse("truncated binary PCD point"));
309 }
310 let field_start = offset;
311 offset += size;
312
313 if field.name.eq_ignore_ascii_case("rgb") && field.count == 1 && field.size == 4 {
314 let chunk = &bytes[field_start..field_start + 4];
315 let bits = u32::from_le_bytes(chunk.try_into().expect("rgb chunk"));
316 push_to_field(buffers, schema, "r", ((bits >> 16) & 0xFF) as f32)?;
317 push_to_field(buffers, schema, "g", ((bits >> 8) & 0xFF) as f32)?;
318 push_to_field(buffers, schema, "b", (bits & 0xFF) as f32)?;
319 continue;
320 }
321
322 for component in 0..field.count {
323 let scalar_start = field_start + component * field.size;
324 let scalar_end = scalar_start + field.size;
325 let value = read_binary_scalar(field, &bytes[scalar_start..scalar_end])?;
326 push_to_field(buffers, schema, &field.name, value)?;
327 }
328 }
329 Ok(())
330}
331
332fn read_binary_scalar(
333 field: &crate::pcd::schema::PcdFieldSpec,
334 chunk: &[u8],
335) -> Result<f32, IoError> {
336 let value = match (field.kind, field.size) {
337 (crate::pcd::schema::PcdType::F, 4) => f32::from_le_bytes(chunk.try_into().expect("f32")),
338 (crate::pcd::schema::PcdType::F, 8) => {
339 f64::from_le_bytes(chunk.try_into().expect("f64")) as f32
340 }
341 (crate::pcd::schema::PcdType::I, 4) => {
342 i32::from_le_bytes(chunk.try_into().expect("i32")) as f32
343 }
344 (crate::pcd::schema::PcdType::U, 1) => f32::from(chunk[0]),
345 (crate::pcd::schema::PcdType::U, 2) => {
346 f32::from(u16::from_le_bytes(chunk.try_into().expect("u16")))
347 }
348 (crate::pcd::schema::PcdType::U, 4) => {
349 u32::from_le_bytes(chunk.try_into().expect("u32")) as f32
350 }
351 _ => return Err(pcd_format(format!("unsupported binary field `{}`", field.name))),
352 };
353 Ok(value)
354}
355
356fn push_to_field(
357 buffers: &mut PointBufferSet,
358 schema: &PointSchema,
359 name: &str,
360 value: f32,
361) -> Result<(), IoError> {
362 let field = schema
363 .fields()
364 .iter()
365 .find(|field| field.name == name)
366 .ok_or_else(|| pcd_format(format!("schema missing mapped field `{name}`")))?;
367
368 let buffer = buffers
369 .get_mut(name)
370 .ok_or_else(|| pcd_format(format!("buffer missing for field `{name}`")))?;
371
372 match field.dtype {
373 DType::F32 | DType::F16 => buffer.push_f32(value).map_err(IoError::from),
374 DType::F64 => buffer.push_f64(f64::from(value)).map_err(IoError::from),
375 DType::U8 => buffer.push_u8(value.round() as u8).map_err(IoError::from),
376 DType::U16 => buffer.push_u16(value.round() as u16).map_err(IoError::from),
377 DType::I32 => buffer.push_i32(value.round() as i32).map_err(IoError::from),
378 DType::U32 => {
379 let PointBuffer::U32(values) = buffer else {
380 return Err(IoError::Core(spatialrust_core::SpatialError::UnsupportedDType(
381 field.dtype,
382 )));
383 };
384 values.push(value.round() as u32);
385 Ok(())
386 }
387 }
388}
389
390pub fn read_pcd_file(path: impl AsRef<std::path::Path>) -> Result<PointCloud, IoError> {
392 let file = std::fs::File::open(path.as_ref())?;
393 let mut reader = std::io::BufReader::new(file);
394 read_pcd(&mut reader)
395}
396
397#[cfg(feature = "streaming")]
401pub struct PcdChunkSource<R: BufRead> {
402 reader: R,
403 header: PcdHeader,
404 metadata: SpatialMetadata,
405 state: FormatStreamState,
406 loaded: usize,
407}
408
409#[cfg(feature = "streaming")]
410impl<R: BufRead> PcdChunkSource<R> {
411 pub fn new(
413 mut reader: R,
414 options: StreamOptions,
415 cancellation: CancellationToken,
416 ) -> Result<Self, IoError> {
417 let (header, _) = PcdHeader::parse(&mut reader)?;
418 if header.data == PcdDataKind::BinaryCompressed {
419 return Err(pcd_format("binary_compressed PCD requires the bounded spool adapter"));
420 }
421 let schema = schema_from_pcd_fields(&header.fields)?;
422 let metadata = metadata_from_header(&header);
423 let state = FormatStreamState::new("pcd", schema, options, cancellation)?;
424 Ok(Self { reader, header, metadata, state, loaded: 0 })
425 }
426
427 #[must_use]
429 pub fn header(&self) -> &PcdHeader {
430 &self.header
431 }
432}
433
434#[cfg(feature = "streaming")]
435impl PcdChunkSource<std::io::BufReader<std::fs::File>> {
436 pub fn open(
438 path: impl AsRef<std::path::Path>,
439 options: StreamOptions,
440 cancellation: CancellationToken,
441 ) -> Result<Self, IoError> {
442 Self::new(std::io::BufReader::new(std::fs::File::open(path)?), options, cancellation)
443 }
444}
445
446#[cfg(feature = "streaming")]
447impl<R: BufRead> BoundedSpatialRecordSource for PcdChunkSource<R> {
448 fn schema(&self) -> &SchemaDescriptor {
449 &self.state.schema
450 }
451
452 fn options(&self) -> &StreamOptions {
453 &self.state.options
454 }
455
456 fn memory_tracker(&self) -> &MemoryTracker {
457 &self.state.tracker
458 }
459
460 fn cancellation_token(&self) -> CancellationToken {
461 self.state.cancellation.clone()
462 }
463
464 fn max_chunk_bytes(&self) -> u64 {
465 self.state.max_chunk_bytes
466 }
467
468 fn next_chunk(&mut self) -> Option<RecordsResult<SpatialRecordChunk>> {
469 if self.loaded == self.header.points {
470 return None;
471 }
472 let count = (self.header.points - self.loaded).min(self.state.options.chunk_points());
473 let scratch = match self.header.data {
474 PcdDataKind::Ascii => 0,
475 PcdDataKind::Binary => match self
476 .header
477 .point_step()
478 .checked_mul(count)
479 .and_then(|bytes| u64::try_from(bytes).ok())
480 {
481 Some(bytes) => bytes,
482 None => {
483 return Some(Err(spatialrust_records::RecordsError::InvalidChunk(
484 "PCD binary chunk size overflow".into(),
485 )));
486 }
487 },
488 PcdDataKind::BinaryCompressed => unreachable!("rejected by constructor"),
489 };
490 let reservation = match self.state.reserve_points_with_scratch(count, scratch) {
491 Ok(reservation) => reservation,
492 Err(error) => return Some(Err(error)),
493 };
494 let schema = self.state.schema.point_schema().clone();
495 let mut buffers = PointBufferSet::new();
496 for field in schema.fields() {
497 buffers.insert(field.name.clone(), PointBuffer::with_capacity(field.dtype, count));
498 }
499
500 let decoded = match self.header.data {
501 PcdDataKind::Ascii => read_ascii_chunk(
502 &mut self.reader,
503 &self.header,
504 &schema,
505 &mut buffers,
506 count,
507 self.loaded,
508 ),
509 PcdDataKind::Binary => {
510 let mut payload = vec![0_u8; self.header.point_step() * count];
511 std::io::Read::read_exact(&mut self.reader, &mut payload)
512 .map_err(IoError::from)
513 .and_then(|()| {
514 payload.chunks_exact(self.header.point_step()).try_for_each(|point| {
515 decode_binary_point(&self.header.fields, point, &schema, &mut buffers)
516 })
517 })
518 }
519 PcdDataKind::BinaryCompressed => unreachable!("rejected by constructor"),
520 };
521 if let Err(error) = decoded {
522 return Some(Err(records_io(error)));
523 }
524 let cloud = match PointCloud::try_from_parts(schema, buffers, self.metadata.clone()) {
525 Ok(cloud) => cloud,
526 Err(error) => return Some(Err(error.into())),
527 };
528 self.loaded += count;
529 Some(self.state.lease(cloud, reservation))
530 }
531}
532
533#[cfg(feature = "streaming")]
534fn read_ascii_chunk<R: BufRead>(
535 reader: &mut R,
536 header: &PcdHeader,
537 schema: &PointSchema,
538 buffers: &mut PointBufferSet,
539 count: usize,
540 point_offset: usize,
541) -> Result<(), IoError> {
542 let mut loaded = 0;
543 let mut line = [0_u8; MAX_ASCII_RECORD_BYTES];
544 while loaded < count {
545 let Some(line) = read_bounded_ascii_line(reader, &mut line, "PCD")? else {
546 return Err(pcd_parse(format!(
547 "unexpected EOF after {} of {} ASCII points",
548 point_offset + loaded,
549 header.points
550 )));
551 };
552 let trimmed = line.trim();
553 if trimmed.is_empty() || trimmed.starts_with('#') {
554 continue;
555 }
556 let mut tokens = trimmed.split_whitespace();
557 for field in &header.fields {
558 if field.name.eq_ignore_ascii_case("rgb") {
559 let packed = parse_packed_rgb(
560 tokens.next().ok_or_else(|| pcd_parse("missing rgb token in ASCII PCD"))?,
561 )?;
562 push_to_field(buffers, schema, "r", packed.0)?;
563 push_to_field(buffers, schema, "g", packed.1)?;
564 push_to_field(buffers, schema, "b", packed.2)?;
565 } else {
566 for _ in 0..field.count {
567 let token = tokens
568 .next()
569 .ok_or_else(|| pcd_parse(format!("missing field `{}`", field.name)))?;
570 let value = token
571 .parse::<f32>()
572 .map_err(|_| pcd_parse(format!("invalid ASCII value `{token}`")))?;
573 push_to_field(buffers, schema, &field.name, value)?;
574 }
575 }
576 }
577 loaded += 1;
578 }
579 Ok(())
580}
581
582#[cfg(test)]
583mod tests {
584 use super::read_pcd;
585 use crate::pcd::writer::{write_pcd, PcdWriteFormat};
586 use spatialrust_core::{HasIntensity, HasPositions3, PointCloudBuilder, StandardSchemas};
587 use std::io::Cursor;
588
589 const SAMPLE_XYZ_ASCII: &str = "\
590# .PCD v0.7 - Point Cloud Data file format
591VERSION 0.7
592FIELDS x y z
593SIZE 4 4 4
594TYPE F F F
595COUNT 1 1 1
596WIDTH 3
597HEIGHT 1
598VIEWPOINT 0 0 0 1 0 0 0
599POINTS 3
600DATA ascii
6010.0 0.0 0.0
6021.0 0.0 0.0
6030.0 1.0 0.0
604";
605
606 const SAMPLE_XYZI_ASCII: &str = "\
607VERSION 0.7
608FIELDS x y z intensity
609SIZE 4 4 4 4
610TYPE F F F F
611COUNT 1 1 1 1
612WIDTH 2
613HEIGHT 1
614VIEWPOINT 0 0 0 1 0 0 0
615POINTS 2
616DATA ascii
6170.0 0.0 0.0 0.5
6181.0 0.0 0.0 0.8
619";
620
621 fn binary_compressed_xyz_sample() -> Vec<u8> {
622 let header = b"\
623# .PCD v0.7 - Point Cloud Data file format
624VERSION 0.7
625FIELDS x y z
626SIZE 4 4 4
627TYPE F F F
628COUNT 1 1 1
629WIDTH 2
630HEIGHT 1
631VIEWPOINT 0 0 0 1 0 0 0
632POINTS 2
633DATA binary_compressed
634";
635 let mut uncompressed = Vec::new();
636 for value in [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0] {
639 uncompressed.extend_from_slice(&value.to_le_bytes());
640 }
641
642 let mut compressed = Vec::with_capacity(uncompressed.len() + 1);
643 compressed.push((uncompressed.len() - 1) as u8);
644 compressed.extend_from_slice(&uncompressed);
645
646 let mut data = header.to_vec();
647 data.extend_from_slice(&(compressed.len() as u32).to_le_bytes());
648 data.extend_from_slice(&(uncompressed.len() as u32).to_le_bytes());
649 data.extend_from_slice(&compressed);
650 data
651 }
652
653 #[test]
654 fn reads_ascii_xyz() {
655 let mut reader = Cursor::new(SAMPLE_XYZ_ASCII.as_bytes());
656 let cloud = read_pcd(&mut reader).unwrap();
657 assert_eq!(cloud.len(), 3);
658 let (x, y, z) = cloud.positions3().unwrap();
659 assert_eq!(x, &[0.0, 1.0, 0.0]);
660 assert_eq!(y, &[0.0, 0.0, 1.0]);
661 assert_eq!(z, &[0.0, 0.0, 0.0]);
662 }
663
664 #[test]
665 fn reads_ascii_xyzi() {
666 let mut reader = Cursor::new(SAMPLE_XYZI_ASCII.as_bytes());
667 let cloud = read_pcd(&mut reader).unwrap();
668 assert_eq!(cloud.intensity().unwrap(), &[0.5, 0.8]);
669 }
670
671 #[test]
672 fn roundtrip_ascii_xyz() {
673 let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyz());
674 builder.push_point([0.0, 0.0, 0.0]).unwrap();
675 builder.push_point([1.0, 2.0, 3.0]).unwrap();
676 let cloud = builder.build().unwrap();
677
678 let mut buffer = Vec::new();
679 write_pcd(&mut buffer, &cloud, PcdWriteFormat::Ascii).unwrap();
680
681 let mut reader = Cursor::new(buffer);
682 let loaded = read_pcd(&mut reader).unwrap();
683 assert_eq!(loaded.len(), cloud.len());
684 let (x, y, z) = loaded.positions3().unwrap();
685 assert_eq!(x, cloud.field("x").unwrap().as_f32().unwrap());
686 assert_eq!(y, cloud.field("y").unwrap().as_f32().unwrap());
687 assert_eq!(z, cloud.field("z").unwrap().as_f32().unwrap());
688 }
689
690 #[test]
691 fn roundtrip_binary_xyz() {
692 let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyz());
693 builder.push_point([0.5, 1.5, 2.5]).unwrap();
694 let cloud = builder.build().unwrap();
695
696 let mut buffer = Vec::new();
697 write_pcd(&mut buffer, &cloud, PcdWriteFormat::Binary).unwrap();
698
699 let mut reader = Cursor::new(buffer);
700 let loaded = read_pcd(&mut reader).unwrap();
701 let (x, _, _) = loaded.positions3().unwrap();
702 assert!((x[0] - 0.5).abs() < 1e-6);
703 }
704
705 #[test]
706 fn reads_binary_compressed_xyz() {
707 let data = binary_compressed_xyz_sample();
708 let mut reader = Cursor::new(data);
709 let loaded = read_pcd(&mut reader).unwrap();
710 let (x, y, z) = loaded.positions3().unwrap();
711 assert_eq!(x, &[1.0, 2.0]);
712 assert_eq!(y, &[3.0, 4.0]);
713 assert_eq!(z, &[5.0, 6.0]);
714 }
715}