git.lucas.co / cce-mesh-io
mesh files in: STL, OBJ, glTF and PLY
git clone https://git.lucas.co/cce-mesh-io.git

src/ply.rs (14K)

  1 //! PLY (Stanford polygon file), ASCII and binary of either byte order.
  2 //!
  3 //! A PLY file declares its own layout: a header lists elements (`vertex`,
  4 //! `face`, and anything else a tool chose to add), each with a count and
  5 //! typed properties, and the body follows in that order. So the reader
  6 //! parses the header into that layout and walks the body by it, keeping
  7 //! `x y z` and any `red green blue` of each vertex and the index list of each
  8 //! face, and reading past everything else — an unknown element still has to
  9 //! be read to find where the next one starts.
 10 //!
 11 //! Colours stored as 8-bit (the usual) are sRGB and are decoded to linear;
 12 //! colours stored as floats are taken as linear already. They ride on
 13 //! corners. A file of points with no faces is an error for now: the viewer
 14 //! draws no point clouds yet. The scene is [`UpAxis::Y`].
 15 
 16 use glam::Vec3;
 17 
 18 use crate::mesh::{srgb_to_linear, Material, Mesh};
 19 use crate::{Part, Scene, Unit, UpAxis};
 20 
 21 #[derive(Debug, Clone, Copy, PartialEq)]
 22 enum Scalar {
 23     I8,
 24     U8,
 25     I16,
 26     U16,
 27     I32,
 28     U32,
 29     F32,
 30     F64,
 31 }
 32 
 33 impl Scalar {
 34     fn parse(name: &str) -> Option<Scalar> {
 35         Some(match name {
 36             "char" | "int8" => Scalar::I8,
 37             "uchar" | "uint8" => Scalar::U8,
 38             "short" | "int16" => Scalar::I16,
 39             "ushort" | "uint16" => Scalar::U16,
 40             "int" | "int32" => Scalar::I32,
 41             "uint" | "uint32" => Scalar::U32,
 42             "float" | "float32" => Scalar::F32,
 43             "double" | "float64" => Scalar::F64,
 44             _ => return None,
 45         })
 46     }
 47 
 48     fn size(self) -> usize {
 49         match self {
 50             Scalar::I8 | Scalar::U8 => 1,
 51             Scalar::I16 | Scalar::U16 => 2,
 52             Scalar::I32 | Scalar::U32 | Scalar::F32 => 4,
 53             Scalar::F64 => 8,
 54         }
 55     }
 56 }
 57 
 58 #[derive(Debug)]
 59 enum Property {
 60     Scalar { name: String, ty: Scalar },
 61     List { name: String, count: Scalar, item: Scalar },
 62 }
 63 
 64 #[derive(Debug)]
 65 struct Element {
 66     name: String,
 67     count: usize,
 68     properties: Vec<Property>,
 69 }
 70 
 71 #[derive(Debug, Clone, Copy, PartialEq)]
 72 enum Encoding {
 73     Ascii,
 74     Little,
 75     Big,
 76 }
 77 
 78 /// The body, read one value at a time in whichever encoding the file uses.
 79 struct Body<'a> {
 80     bytes: &'a [u8],
 81     at: usize,
 82     encoding: Encoding,
 83 }
 84 
 85 impl Body<'_> {
 86     fn value(&mut self, ty: Scalar) -> Result<f64, String> {
 87         if self.encoding == Encoding::Ascii {
 88             return self.word().and_then(|w| w.parse::<f64>().map_err(|_| format!("'{w}' is not a number")));
 89         }
 90         let n = ty.size();
 91         let raw = self.bytes.get(self.at..self.at + n).ok_or("the file ends in the middle of its data")?;
 92         self.at += n;
 93         let mut b = [0u8; 8];
 94         b[..n].copy_from_slice(raw);
 95         if self.encoding == Encoding::Big {
 96             b[..n].reverse();
 97         }
 98         Ok(match ty {
 99             Scalar::I8 => b[0] as i8 as f64,
100             Scalar::U8 => b[0] as f64,
101             Scalar::I16 => i16::from_le_bytes([b[0], b[1]]) as f64,
102             Scalar::U16 => u16::from_le_bytes([b[0], b[1]]) as f64,
103             Scalar::I32 => i32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64,
104             Scalar::U32 => u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64,
105             Scalar::F32 => f32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64,
106             Scalar::F64 => f64::from_le_bytes(b),
107         })
108     }
109 
110     fn word(&mut self) -> Result<&str, String> {
111         let rest = &self.bytes[self.at..];
112         let start = rest.iter().position(|c| !c.is_ascii_whitespace()).ok_or("the file ends in the middle of its data")?;
113         let len = rest[start..].iter().position(|c| c.is_ascii_whitespace()).unwrap_or(rest.len() - start);
114         self.at += start + len;
115         std::str::from_utf8(&rest[start..start + len]).map_err(|_| "text that is not UTF-8".to_string())
116     }
117 }
118 
119 pub fn read(bytes: &[u8]) -> Result<Scene, String> {
120     let (elements, encoding, body_at) = header(bytes)?;
121     let mut body = Body { bytes, at: body_at, encoding };
122     let mut positions: Vec<Vec3> = Vec::new();
123     let mut point_colors: Vec<[f32; 3]> = Vec::new();
124     let mut faces: Vec<[u32; 3]> = Vec::new();
125     let mut corner: Vec<u32> = Vec::new();
126 
127     for el in &elements {
128         match el.name.as_str() {
129             "vertex" => {
130                 let at = |n: &str| {
131                     el.properties.iter().position(|p| matches!(p, Property::Scalar { name, .. } if name == n))
132                 };
133                 let (Some(xi), Some(yi), Some(zi)) = (at("x"), at("y"), at("z")) else {
134                     return Err("vertices without x, y and z".into());
135                 };
136                 let rgb = match (at("red"), at("green"), at("blue")) {
137                     (Some(r), Some(g), Some(b)) => Some([r, g, b]),
138                     _ => match (at("diffuse_red"), at("diffuse_green"), at("diffuse_blue")) {
139                         (Some(r), Some(g), Some(b)) => Some([r, g, b]),
140                         _ => None,
141                     },
142                 };
143                 positions.reserve(el.count);
144                 let mut row = vec![0f64; el.properties.len()];
145                 for _ in 0..el.count {
146                     for (i, p) in el.properties.iter().enumerate() {
147                         row[i] = match p {
148                             Property::Scalar { ty, .. } => body.value(*ty)?,
149                             Property::List { count, item, .. } => {
150                                 skip_list(&mut body, *count, *item)?;
151                                 0.0
152                             }
153                         };
154                     }
155                     positions.push(Vec3::new(row[xi] as f32, row[yi] as f32, row[zi] as f32));
156                     if let Some(idx) = rgb {
157                         point_colors.push(idx.map(|i| match &el.properties[i] {
158                             Property::Scalar { ty: Scalar::F32 | Scalar::F64, .. } => row[i] as f32,
159                             _ => srgb_to_linear(row[i].clamp(0.0, 255.0) as u8),
160                         }));
161                     }
162                 }
163             }
164             "face" => {
165                 faces.reserve(el.count);
166                 for _ in 0..el.count {
167                     for p in &el.properties {
168                         match p {
169                             Property::List { name, count, item } if name == "vertex_indices" || name == "vertex_index" => {
170                                 let n = body.value(*count)? as usize;
171                                 corner.clear();
172                                 for _ in 0..n {
173                                     corner.push(body.value(*item)? as u32);
174                                 }
175                                 for k in 1..n.saturating_sub(1) {
176                                     faces.push([corner[0], corner[k], corner[k + 1]]);
177                                 }
178                             }
179                             Property::List { count, item, .. } => skip_list(&mut body, *count, *item)?,
180                             Property::Scalar { ty, .. } => {
181                                 body.value(*ty)?;
182                             }
183                         }
184                     }
185                 }
186             }
187             _ => {
188                 for _ in 0..el.count {
189                     for p in &el.properties {
190                         match p {
191                             Property::Scalar { ty, .. } => {
192                                 body.value(*ty)?;
193                             }
194                             Property::List { count, item, .. } => skip_list(&mut body, *count, *item)?,
195                         }
196                     }
197                 }
198             }
199         }
200     }
201 
202     if faces.is_empty() {
203         return Err(if positions.is_empty() {
204             "no vertices and no faces".into()
205         } else {
206             format!("{} points and no faces: point clouds are not drawn yet", positions.len())
207         });
208     }
209     let n = positions.len() as u32;
210     if let Some(bad) = faces.iter().flatten().find(|&&i| i >= n) {
211         return Err(format!("a face names vertex {bad}, but there are {n}"));
212     }
213     let corner_colors = (!point_colors.is_empty())
214         .then(|| faces.iter().flat_map(|f| f.map(|i| point_colors[i as usize])).collect());
215     let mesh = Mesh {
216         tri_material: vec![0; faces.len()],
217         positions,
218         triangles: faces,
219         materials: vec![Material::default()],
220         corner_colors,
221         corner_uvs: None,
222         file_normals: None,
223     };
224     Ok(Scene { parts: vec![Part { name: String::new(), mesh }], up: UpAxis::Y, unit: Unit::Unspecified, textures: Vec::new() })
225 }
226 
227 fn skip_list(body: &mut Body, count: Scalar, item: Scalar) -> Result<(), String> {
228     let n = body.value(count)? as usize;
229     for _ in 0..n {
230         body.value(item)?;
231     }
232     Ok(())
233 }
234 
235 /// The elements, the encoding, and where the body starts.
236 fn header(bytes: &[u8]) -> Result<(Vec<Element>, Encoding, usize), String> {
237     if !bytes.starts_with(b"ply") {
238         return Err("not a PLY file (it does not begin with 'ply')".into());
239     }
240     let end = bytes
241         .windows(10)
242         .position(|w| w == b"end_header")
243         .ok_or("the header never ends (no 'end_header')")?;
244     // The body starts after the line end that follows end_header (\n or \r\n).
245     let mut body_at = end + 10;
246     while bytes.get(body_at).is_some_and(|&c| c == b' ' || c == b'\r') {
247         body_at += 1;
248     }
249     if bytes.get(body_at) == Some(&b'\n') {
250         body_at += 1;
251     }
252     let text = std::str::from_utf8(&bytes[..end]).map_err(|_| "a header that is not text".to_string())?;
253     let mut encoding = None;
254     let mut elements: Vec<Element> = Vec::new();
255     for line in text.lines().skip(1) {
256         let w: Vec<&str> = line.split_whitespace().collect();
257         match w.as_slice() {
258             ["format", "ascii", _] => encoding = Some(Encoding::Ascii),
259             ["format", "binary_little_endian", _] => encoding = Some(Encoding::Little),
260             ["format", "binary_big_endian", _] => encoding = Some(Encoding::Big),
261             ["format", other, ..] => return Err(format!("unknown PLY format '{other}'")),
262             ["element", name, count] => elements.push(Element {
263                 name: name.to_string(),
264                 count: count.parse().map_err(|_| format!("element {name} has count '{count}'"))?,
265                 properties: Vec::new(),
266             }),
267             ["property", "list", count, item, name] => {
268                 let el = elements.last_mut().ok_or("a property before any element")?;
269                 let ty = |t: &str| Scalar::parse(t).ok_or_else(|| format!("unknown property type '{t}'"));
270                 el.properties.push(Property::List { name: name.to_string(), count: ty(count)?, item: ty(item)? });
271             }
272             ["property", ty, name] => {
273                 let el = elements.last_mut().ok_or("a property before any element")?;
274                 let ty = Scalar::parse(ty).ok_or_else(|| format!("unknown property type '{ty}'"))?;
275                 el.properties.push(Property::Scalar { name: name.to_string(), ty });
276             }
277             ["comment", ..] | ["obj_info", ..] | [] => {}
278             _ => log::debug!("[mesh-io] ply: skipped header line {line:?}"),
279         }
280     }
281     Ok((elements, encoding.ok_or("no 'format' line")?, body_at))
282 }
283 
284 #[cfg(test)]
285 mod tests {
286     use super::*;
287 
288     const ASCII_QUAD: &str = "ply\nformat ascii 1.0\ncomment made by hand\nelement vertex 4\nproperty float x\nproperty float y\nproperty float z\nproperty uchar red\nproperty uchar green\nproperty uchar blue\nelement face 1\nproperty list uchar int vertex_indices\nend_header\n0 0 0 255 0 0\n1 0 0 255 0 0\n1 1 0 0 0 255\n0 1 0 0 0 255\n4 0 1 2 3\n";
289 
290     fn mesh(bytes: &[u8]) -> Mesh {
291         read(bytes).unwrap().parts.remove(0).mesh
292     }
293 
294     #[test]
295     fn an_ascii_quad_is_two_coloured_triangles() {
296         let m = mesh(ASCII_QUAD.as_bytes());
297         assert_eq!(m.triangles, vec![[0, 1, 2], [0, 2, 3]]);
298         let c = m.corner_colors.unwrap();
299         assert_eq!(c[0], [1.0, 0.0, 0.0]);
300         assert_eq!(c[2], [0.0, 0.0, 1.0], "corner 2 is vertex 2, blue");
301     }
302 
303     /// The quad again, binary, with an extra element in the way and a
304     /// per-face list it must read past.
305     fn binary_quad(big: bool) -> Vec<u8> {
306         let fmt = if big { "binary_big_endian" } else { "binary_little_endian" };
307         let mut b = format!(
308             "ply\r\nformat {fmt} 1.0\r\nelement vertex 4\r\nproperty double x\r\nproperty double y\r\nproperty double z\r\nelement face 1\r\nproperty list uchar uint vertex_indices\r\nproperty list uchar float texcoord\r\nelement extra 1\r\nproperty short junk\r\nend_header\r\n"
309         )
310         .into_bytes();
311         let f64b = |v: f64| if big { v.to_be_bytes() } else { v.to_le_bytes() };
312         let u32b = |v: u32| if big { v.to_be_bytes() } else { v.to_le_bytes() };
313         for p in [[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.]] {
314             for c in p {
315                 b.extend(f64b(c));
316             }
317         }
318         b.push(4);
319         for i in [0, 1, 2, 3] {
320             b.extend(u32b(i));
321         }
322         b.push(2);
323         b.extend([0u8; 8]);
324         b.extend([7u8, 7]);
325         b
326     }
327 
328     #[test]
329     fn binary_of_either_byte_order() {
330         for big in [false, true] {
331             let m = mesh(&binary_quad(big));
332             assert_eq!(m.triangles, vec![[0, 1, 2], [0, 2, 3]], "big endian: {big}");
333             assert_eq!(m.positions[2], Vec3::new(1.0, 1.0, 0.0));
334             assert!(m.corner_colors.is_none());
335         }
336     }
337 
338     #[test]
339     fn a_point_cloud_says_so() {
340         let text = "ply\nformat ascii 1.0\nelement vertex 2\nproperty float x\nproperty float y\nproperty float z\nend_header\n0 0 0\n1 1 1\n";
341         let e = read(text.as_bytes()).unwrap_err();
342         assert!(e.contains("point clouds"), "{e}");
343     }
344 
345     #[test]
346     fn a_short_file_and_a_bad_index_are_errors() {
347         let mut b = binary_quad(false);
348         b.truncate(b.len() - 20);
349         assert!(read(&b).unwrap_err().contains("ends in the middle"));
350         let bad = ASCII_QUAD.replace("4 0 1 2 3", "3 0 1 9");
351         assert!(read(bad.as_bytes()).unwrap_err().contains("names vertex 9"));
352     }
353 }