Skip to main content

bunny_codec/
ply.rs

1use std::fmt;
2use std::str;
3
4use bunny_mesh::Triangle32;
5
6mod header;
7
8use header::parse_header;
9
10const VERTEX_STRIDE: usize = 12;
11const TRIANGLE_FACE_STRIDE: usize = 13;
12
13/// Error returned when a binary PLY mesh cannot be parsed as the Bunny mesh profile.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum PlyError {
16    /// The `end_header` marker was not found.
17    MissingHeaderEnd,
18    /// The header is not valid UTF-8 text.
19    HeaderUtf8,
20    /// The first header line is not `ply`.
21    InvalidMagic,
22    /// The `format` line is absent or is not `binary_little_endian 1.0`.
23    UnsupportedFormat,
24    /// An element count is missing or not a non-negative decimal integer.
25    InvalidCount,
26    /// The header does not declare the canonical vertex element.
27    MissingVertexElement,
28    /// The header does not declare the canonical face element.
29    MissingFaceElement,
30    /// The header declares an unsupported non-empty element.
31    UnsupportedElement,
32    /// The header declares an unsupported property layout.
33    UnsupportedProperty,
34    /// The binary payload is shorter than the declared mesh layout.
35    PayloadTooShort,
36    /// The binary payload contains bytes after the declared mesh layout.
37    TrailingData,
38    /// A face list entry is not a triangle.
39    NonTriangularFace,
40    /// A face index is negative and cannot be represented as `u32`.
41    NegativeIndex,
42    /// A face references a vertex outside the parsed vertex range.
43    IndexOutOfBounds,
44    /// A vertex coordinate is NaN or infinity.
45    NonFiniteVertex,
46    /// A declared count or offset overflowed `usize`.
47    IntegerOverflow,
48}
49
50impl fmt::Display for PlyError {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        let message = match self {
53            Self::MissingHeaderEnd => "PLY header terminator was not found",
54            Self::HeaderUtf8 => "PLY header is not valid UTF-8",
55            Self::InvalidMagic => "PLY header does not start with ply",
56            Self::UnsupportedFormat => "PLY format must be binary_little_endian 1.0",
57            Self::InvalidCount => "PLY element count is invalid",
58            Self::MissingVertexElement => "PLY vertex element is missing or incomplete",
59            Self::MissingFaceElement => "PLY face element is missing or incomplete",
60            Self::UnsupportedElement => "PLY declares an unsupported non-empty element",
61            Self::UnsupportedProperty => "PLY property layout is unsupported",
62            Self::PayloadTooShort => "PLY binary payload is shorter than declared",
63            Self::TrailingData => "PLY binary payload has trailing bytes",
64            Self::NonTriangularFace => "PLY face list entry is not a triangle",
65            Self::NegativeIndex => "PLY face index is negative",
66            Self::IndexOutOfBounds => "PLY face index is out of bounds",
67            Self::NonFiniteVertex => "PLY vertex coordinate is not finite",
68            Self::IntegerOverflow => "PLY count or offset overflowed usize",
69        };
70        f.write_str(message)
71    }
72}
73
74impl std::error::Error for PlyError {}
75
76/// A borrowed binary PLY mesh with canonical `float x/y/z` vertices and triangle faces.
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub struct PlyBinaryMesh<'a> {
79    vertex_bytes: &'a [u8],
80    face_bytes: &'a [u8],
81    vertex_count: usize,
82    face_count: usize,
83}
84
85impl<'a> PlyBinaryMesh<'a> {
86    /// Returns the number of vertices declared by the PLY file.
87    #[must_use]
88    pub const fn vertex_count(self) -> usize {
89        self.vertex_count
90    }
91
92    /// Returns the number of triangle faces declared by the PLY file.
93    #[must_use]
94    pub const fn face_count(self) -> usize {
95        self.face_count
96    }
97
98    /// Returns the borrowed binary vertex payload.
99    #[must_use]
100    pub const fn vertex_bytes(self) -> &'a [u8] {
101        self.vertex_bytes
102    }
103
104    /// Returns the borrowed binary face payload.
105    #[must_use]
106    pub const fn face_bytes(self) -> &'a [u8] {
107        self.face_bytes
108    }
109
110    /// Reads a vertex from the borrowed payload.
111    ///
112    /// # Errors
113    /// Returns `PlyError::PayloadTooShort` if the requested vertex is out of range.
114    pub fn vertex(self, index: usize) -> Result<PlyVertex, PlyError> {
115        let start = checked_offset(index, VERTEX_STRIDE)?;
116        let bytes = take(self.vertex_bytes, start, VERTEX_STRIDE)?;
117        read_vertex(bytes)
118    }
119
120    /// Reads a triangle face from the borrowed payload.
121    ///
122    /// # Errors
123    /// Returns a `PlyError` if the face is out of range, is not triangular, or
124    /// contains negative signed PLY indices.
125    pub fn triangle(self, index: usize) -> Result<Triangle32, PlyError> {
126        let start = checked_offset(index, TRIANGLE_FACE_STRIDE)?;
127        let bytes = take(self.face_bytes, start, TRIANGLE_FACE_STRIDE)?;
128        let triangle = read_triangle(bytes)?;
129        validate_triangle_bounds(triangle, self.vertex_count)?;
130        Ok(triangle)
131    }
132}
133
134/// A PLY vertex decoded from borrowed little-endian float bytes.
135#[derive(Clone, Copy, Debug, PartialEq)]
136pub struct PlyVertex {
137    /// X coordinate.
138    pub x: f32,
139    /// Y coordinate.
140    pub y: f32,
141    /// Z coordinate.
142    pub z: f32,
143}
144
145struct PlyPayload<'a> {
146    vertex_bytes: &'a [u8],
147    face_bytes: &'a [u8],
148    payload_end: usize,
149}
150
151impl PlyVertex {
152    const fn is_finite(self) -> bool {
153        self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
154    }
155}
156
157/// Parses a canonical binary little-endian PLY mesh as a zero-copy borrowed view.
158///
159/// The accepted profile is `float x`, `float y`, `float z` vertices and
160/// `property list uchar int vertex_indices` triangle faces.
161///
162/// # Errors
163/// Returns `PlyError` when the header or binary payload does not match the
164/// canonical Bunny mesh PLY profile.
165pub fn parse_binary_ply(input: &[u8]) -> Result<PlyBinaryMesh<'_>, PlyError> {
166    let (header, payload_start) = split_header(input)?;
167    let spec = parse_header(header)?;
168    let payload = split_payload(input, payload_start, spec.vertex_count, spec.face_count)?;
169    validate_vertices(payload.vertex_bytes, spec.vertex_count)?;
170    validate_faces(payload.face_bytes, spec.face_count, spec.vertex_count)?;
171    if input.len() != payload.payload_end {
172        return Err(PlyError::TrailingData);
173    }
174
175    Ok(PlyBinaryMesh {
176        vertex_bytes: payload.vertex_bytes,
177        face_bytes: payload.face_bytes,
178        vertex_count: spec.vertex_count,
179        face_count: spec.face_count,
180    })
181}
182
183fn split_payload(
184    input: &[u8],
185    payload_start: usize,
186    vertex_count: usize,
187    face_count: usize,
188) -> Result<PlyPayload<'_>, PlyError> {
189    let vertex_len = checked_offset(vertex_count, VERTEX_STRIDE)?;
190    let face_len = checked_offset(face_count, TRIANGLE_FACE_STRIDE)?;
191    let face_start = payload_start.checked_add(vertex_len).ok_or(PlyError::IntegerOverflow)?;
192    let payload_end = face_start.checked_add(face_len).ok_or(PlyError::IntegerOverflow)?;
193    if input.len() < payload_end {
194        return Err(PlyError::PayloadTooShort);
195    }
196
197    let vertex_bytes = take(input, payload_start, vertex_len)?;
198    let face_bytes = take(input, face_start, face_len)?;
199    Ok(PlyPayload { vertex_bytes, face_bytes, payload_end })
200}
201
202fn split_header(input: &[u8]) -> Result<(&str, usize), PlyError> {
203    let (header_end, payload_start) = header_bounds(input)?;
204    let header = input.get(..header_end).ok_or(PlyError::MissingHeaderEnd)?;
205    str::from_utf8(header).map(|header| (header, payload_start)).map_err(|_| PlyError::HeaderUtf8)
206}
207
208fn header_bounds(input: &[u8]) -> Result<(usize, usize), PlyError> {
209    let mut line_start = 0;
210    while line_start < input.len() {
211        let tail = input.get(line_start..).ok_or(PlyError::MissingHeaderEnd)?;
212        let Some(relative_newline) = tail.iter().position(|byte| *byte == b'\n') else {
213            return Err(PlyError::MissingHeaderEnd);
214        };
215        let line_end = line_start + relative_newline;
216        let previous = line_end.checked_sub(1).and_then(|index| input.get(index)).copied();
217        let content_end =
218            if line_end > line_start && previous == Some(b'\r') { line_end - 1 } else { line_end };
219        if input.get(line_start..content_end) == Some(b"end_header".as_slice()) {
220            return Ok((line_start, line_end + 1));
221        }
222        line_start = line_end + 1;
223    }
224
225    Err(PlyError::MissingHeaderEnd)
226}
227
228fn checked_offset(index: usize, stride: usize) -> Result<usize, PlyError> {
229    index.checked_mul(stride).ok_or(PlyError::IntegerOverflow)
230}
231
232fn take(input: &[u8], start: usize, len: usize) -> Result<&[u8], PlyError> {
233    let end = start.checked_add(len).ok_or(PlyError::IntegerOverflow)?;
234    input.get(start..end).ok_or(PlyError::PayloadTooShort)
235}
236
237fn take_array<const N: usize>(input: &[u8], start: usize) -> Result<[u8; N], PlyError> {
238    let slice = take(input, start, N)?;
239    let mut bytes = [0_u8; N];
240    bytes.copy_from_slice(slice);
241    Ok(bytes)
242}
243
244fn validate_vertices(vertex_bytes: &[u8], count: usize) -> Result<(), PlyError> {
245    for index in 0..count {
246        let start = checked_offset(index, VERTEX_STRIDE)?;
247        read_vertex(take(vertex_bytes, start, VERTEX_STRIDE)?)?;
248    }
249    Ok(())
250}
251
252fn validate_faces(face_bytes: &[u8], count: usize, vertex_count: usize) -> Result<(), PlyError> {
253    for index in 0..count {
254        let start = checked_offset(index, TRIANGLE_FACE_STRIDE)?;
255        let triangle = read_triangle(take(face_bytes, start, TRIANGLE_FACE_STRIDE)?)?;
256        validate_triangle_bounds(triangle, vertex_count)?;
257    }
258    Ok(())
259}
260
261fn read_vertex(bytes: &[u8]) -> Result<PlyVertex, PlyError> {
262    let vertex = PlyVertex {
263        x: f32::from_le_bytes(take_array(bytes, 0)?),
264        y: f32::from_le_bytes(take_array(bytes, 4)?),
265        z: f32::from_le_bytes(take_array(bytes, 8)?),
266    };
267    if vertex.is_finite() {
268        Ok(vertex)
269    } else {
270        Err(PlyError::NonFiniteVertex)
271    }
272}
273
274fn read_triangle(bytes: &[u8]) -> Result<Triangle32, PlyError> {
275    if take(bytes, 0, 1)?.first().copied() != Some(3) {
276        return Err(PlyError::NonTriangularFace);
277    }
278    Ok(Triangle32::new(read_index(bytes, 1)?, read_index(bytes, 5)?, read_index(bytes, 9)?))
279}
280
281fn read_index(bytes: &[u8], start: usize) -> Result<u32, PlyError> {
282    let value = i32::from_le_bytes(take_array(bytes, start)?);
283    u32::try_from(value).map_err(|_| PlyError::NegativeIndex)
284}
285
286fn validate_triangle_bounds(triangle: Triangle32, vertex_count: usize) -> Result<(), PlyError> {
287    if index_is_valid(triangle.v0, vertex_count)
288        && index_is_valid(triangle.v1, vertex_count)
289        && index_is_valid(triangle.v2, vertex_count)
290    {
291        Ok(())
292    } else {
293        Err(PlyError::IndexOutOfBounds)
294    }
295}
296
297fn index_is_valid(index: u32, vertex_count: usize) -> bool {
298    usize::try_from(index).is_ok_and(|index| index < vertex_count)
299}