spatialrust_io/pcd/
header.rs1use std::io::{BufRead, Read};
2
3use crate::error::{pcd_format, pcd_parse, IoError};
4use crate::pcd::schema::PcdFieldSpec;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum PcdDataKind {
9 Ascii,
11 Binary,
13 BinaryCompressed,
15}
16
17#[derive(Clone, Debug, PartialEq)]
19pub struct PcdHeader {
20 pub version: String,
22 pub fields: Vec<PcdFieldSpec>,
24 pub width: usize,
26 pub height: usize,
28 pub viewpoint: [f32; 7],
30 pub points: usize,
32 pub data: PcdDataKind,
34}
35
36impl PcdHeader {
37 #[must_use]
39 pub fn point_step(&self) -> usize {
40 self.fields.iter().map(PcdFieldSpec::byte_size).sum()
41 }
42
43 #[allow(unused_assignments)]
45 pub fn parse<R: BufRead>(reader: &mut R) -> Result<(Self, usize), IoError> {
46 let mut version = String::new();
47 let mut field_names = Vec::new();
48 let mut sizes = Vec::new();
49 let mut kinds = Vec::new();
50 let mut counts = Vec::new();
51 let mut width = 0usize;
52 let mut height = 0usize;
53 let mut viewpoint = [0.0_f32; 7];
54 let mut points = 0usize;
55 let mut data: Option<PcdDataKind> = None;
56 let mut header_bytes = 0usize;
57
58 loop {
59 let mut line = String::new();
60 let read = reader.read_line(&mut line)?;
61 if read == 0 {
62 return Err(pcd_parse("unexpected EOF while reading PCD header"));
63 }
64 header_bytes += read;
65
66 let trimmed = line.trim();
67 if trimmed.is_empty() || trimmed.starts_with('#') {
68 continue;
69 }
70
71 if let Some(rest) = trimmed.strip_prefix("VERSION ") {
72 version = rest.trim().to_owned();
73 continue;
74 }
75 if let Some(rest) = trimmed.strip_prefix("FIELDS ") {
76 field_names = rest.split_whitespace().map(str::to_owned).collect();
77 continue;
78 }
79 if let Some(rest) = trimmed.strip_prefix("SIZE ") {
80 sizes = parse_usize_list(rest)?;
81 continue;
82 }
83 if let Some(rest) = trimmed.strip_prefix("TYPE ") {
84 kinds = parse_type_list(rest)?;
85 continue;
86 }
87 if let Some(rest) = trimmed.strip_prefix("COUNT ") {
88 counts = parse_usize_list(rest)?;
89 continue;
90 }
91 if let Some(rest) = trimmed.strip_prefix("WIDTH ") {
92 width = rest
93 .trim()
94 .parse()
95 .map_err(|_| pcd_parse(format!("invalid WIDTH `{rest}`")))?;
96 continue;
97 }
98 if let Some(rest) = trimmed.strip_prefix("HEIGHT ") {
99 height = rest
100 .trim()
101 .parse()
102 .map_err(|_| pcd_parse(format!("invalid HEIGHT `{rest}`")))?;
103 continue;
104 }
105 if let Some(rest) = trimmed.strip_prefix("VIEWPOINT ") {
106 let values = parse_f32_list(rest)?;
107 if values.len() != 7 {
108 return Err(pcd_parse("VIEWPOINT must contain 7 values"));
109 }
110 viewpoint.copy_from_slice(&values);
111 continue;
112 }
113 if let Some(rest) = trimmed.strip_prefix("POINTS ") {
114 points = rest
115 .trim()
116 .parse()
117 .map_err(|_| pcd_parse(format!("invalid POINTS `{rest}`")))?;
118 continue;
119 }
120 if let Some(rest) = trimmed.strip_prefix("DATA ") {
121 data = Some(match rest.trim().to_ascii_lowercase().as_str() {
122 "ascii" => PcdDataKind::Ascii,
123 "binary" => PcdDataKind::Binary,
124 "binary_compressed" => PcdDataKind::BinaryCompressed,
125 other => return Err(pcd_format(format!("unsupported DATA mode `{other}`"))),
126 });
127 break;
128 }
129
130 return Err(pcd_parse(format!("unexpected PCD header line `{trimmed}`")));
131 }
132
133 if version.is_empty() {
134 version = "0.7".to_owned();
135 }
136 if field_names.is_empty() {
137 return Err(pcd_parse("PCD header missing FIELDS"));
138 }
139 if sizes.len() != field_names.len()
140 || kinds.len() != field_names.len()
141 || counts.len() != field_names.len()
142 {
143 return Err(pcd_parse("FIELDS/SIZE/TYPE/COUNT length mismatch in PCD header"));
144 }
145
146 let fields = field_names
147 .into_iter()
148 .zip(sizes)
149 .zip(kinds)
150 .zip(counts)
151 .map(|(((name, size), kind), count)| PcdFieldSpec { name, size, kind, count })
152 .collect();
153
154 let data_kind = data.ok_or_else(|| pcd_parse("PCD header missing DATA"))?;
155 if points == 0 && width > 0 && height > 0 {
156 points = width * height;
157 }
158
159 Ok((
160 Self { version, fields, width, height, viewpoint, points, data: data_kind },
161 header_bytes,
162 ))
163 }
164}
165
166fn parse_usize_list(input: &str) -> Result<Vec<usize>, IoError> {
167 input
168 .split_whitespace()
169 .map(|value| {
170 value.parse().map_err(|_| pcd_parse(format!("invalid integer `{value}` in PCD header")))
171 })
172 .collect()
173}
174
175fn parse_f32_list(input: &str) -> Result<Vec<f32>, IoError> {
176 input
177 .split_whitespace()
178 .map(|value| {
179 value.parse().map_err(|_| pcd_parse(format!("invalid float `{value}` in PCD header")))
180 })
181 .collect()
182}
183
184fn parse_type_list(input: &str) -> Result<Vec<crate::pcd::schema::PcdType>, IoError> {
185 input
186 .split_whitespace()
187 .map(|value| match value {
188 "I" => Ok(crate::pcd::schema::PcdType::I),
189 "U" => Ok(crate::pcd::schema::PcdType::U),
190 "F" => Ok(crate::pcd::schema::PcdType::F),
191 _ => Err(pcd_parse(format!("invalid TYPE token `{value}`"))),
192 })
193 .collect()
194}
195
196pub fn read_binary_payload<R: Read>(reader: &mut R, byte_len: usize) -> Result<Vec<u8>, IoError> {
198 let mut data = vec![0_u8; byte_len];
199 reader.read_exact(&mut data)?;
200 Ok(data)
201}
202
203#[cfg(test)]
204mod tests {
205 use super::{PcdDataKind, PcdHeader};
206 use std::io::Cursor;
207
208 const SAMPLE_ASCII_HEADER: &str = "\
209# .PCD v0.7 - Point Cloud Data file format
210VERSION 0.7
211FIELDS x y z
212SIZE 4 4 4
213TYPE F F F
214COUNT 1 1 1
215WIDTH 2
216HEIGHT 1
217VIEWPOINT 0 0 0 1 0 0 0
218POINTS 2
219DATA ascii
220";
221
222 #[test]
223 fn parses_ascii_header() {
224 let mut reader = Cursor::new(SAMPLE_ASCII_HEADER);
225 let (header, _) = PcdHeader::parse(&mut reader).unwrap();
226 assert_eq!(header.points, 2);
227 assert_eq!(header.data, PcdDataKind::Ascii);
228 assert_eq!(header.fields.len(), 3);
229 }
230}