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
145impl PlyVertex {
146    const fn is_finite(self) -> bool {
147        self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
148    }
149}
150
151/// Parses a canonical binary little-endian PLY mesh as a zero-copy borrowed view.
152///
153/// The accepted profile is `float x`, `float y`, `float z` vertices and
154/// `property list uchar int vertex_indices` triangle faces.
155///
156/// # Errors
157/// Returns `PlyError` when the header or binary payload does not match the
158/// canonical Bunny mesh PLY profile.
159pub fn parse_binary_ply(input: &[u8]) -> Result<PlyBinaryMesh<'_>, PlyError> {
160    let (header, payload_start) = split_header(input)?;
161    let spec = parse_header(header)?;
162    let vertex_len = checked_offset(spec.vertex_count, VERTEX_STRIDE)?;
163    let face_len = checked_offset(spec.face_count, TRIANGLE_FACE_STRIDE)?;
164    let face_start = payload_start
165        .checked_add(vertex_len)
166        .ok_or(PlyError::IntegerOverflow)?;
167    let payload_end = face_start
168        .checked_add(face_len)
169        .ok_or(PlyError::IntegerOverflow)?;
170    if input.len() < payload_end {
171        return Err(PlyError::PayloadTooShort);
172    }
173
174    let vertex_bytes = take(input, payload_start, vertex_len)?;
175    let face_bytes = take(input, face_start, face_len)?;
176    validate_vertices(vertex_bytes, spec.vertex_count)?;
177    validate_faces(face_bytes, spec.face_count, spec.vertex_count)?;
178    if input.len() != payload_end {
179        return Err(PlyError::TrailingData);
180    }
181
182    Ok(PlyBinaryMesh {
183        vertex_bytes,
184        face_bytes,
185        vertex_count: spec.vertex_count,
186        face_count: spec.face_count,
187    })
188}
189
190fn split_header(input: &[u8]) -> Result<(&str, usize), PlyError> {
191    let (header_end, payload_start) = header_bounds(input)?;
192    let header = input.get(..header_end).ok_or(PlyError::MissingHeaderEnd)?;
193    str::from_utf8(header)
194        .map(|header| (header, payload_start))
195        .map_err(|_| PlyError::HeaderUtf8)
196}
197
198fn header_bounds(input: &[u8]) -> Result<(usize, usize), PlyError> {
199    let mut line_start = 0;
200    while line_start < input.len() {
201        let Some(relative_newline) = input[line_start..].iter().position(|byte| *byte == b'\n')
202        else {
203            return Err(PlyError::MissingHeaderEnd);
204        };
205        let line_end = line_start + relative_newline;
206        let content_end = if line_end > line_start && input[line_end - 1] == b'\r' {
207            line_end - 1
208        } else {
209            line_end
210        };
211        if input.get(line_start..content_end) == Some(b"end_header".as_slice()) {
212            return Ok((line_start, line_end + 1));
213        }
214        line_start = line_end + 1;
215    }
216
217    Err(PlyError::MissingHeaderEnd)
218}
219
220fn checked_offset(index: usize, stride: usize) -> Result<usize, PlyError> {
221    index.checked_mul(stride).ok_or(PlyError::IntegerOverflow)
222}
223
224fn take(input: &[u8], start: usize, len: usize) -> Result<&[u8], PlyError> {
225    let end = start.checked_add(len).ok_or(PlyError::IntegerOverflow)?;
226    input.get(start..end).ok_or(PlyError::PayloadTooShort)
227}
228
229fn take_array<const N: usize>(input: &[u8], start: usize) -> Result<[u8; N], PlyError> {
230    let slice = take(input, start, N)?;
231    let mut bytes = [0_u8; N];
232    bytes.copy_from_slice(slice);
233    Ok(bytes)
234}
235
236fn validate_vertices(vertex_bytes: &[u8], count: usize) -> Result<(), PlyError> {
237    for index in 0..count {
238        let start = checked_offset(index, VERTEX_STRIDE)?;
239        read_vertex(take(vertex_bytes, start, VERTEX_STRIDE)?)?;
240    }
241    Ok(())
242}
243
244fn validate_faces(face_bytes: &[u8], count: usize, vertex_count: usize) -> Result<(), PlyError> {
245    for index in 0..count {
246        let start = checked_offset(index, TRIANGLE_FACE_STRIDE)?;
247        let triangle = read_triangle(take(face_bytes, start, TRIANGLE_FACE_STRIDE)?)?;
248        validate_triangle_bounds(triangle, vertex_count)?;
249    }
250    Ok(())
251}
252
253fn read_vertex(bytes: &[u8]) -> Result<PlyVertex, PlyError> {
254    let vertex = PlyVertex {
255        x: f32::from_le_bytes(take_array(bytes, 0)?),
256        y: f32::from_le_bytes(take_array(bytes, 4)?),
257        z: f32::from_le_bytes(take_array(bytes, 8)?),
258    };
259    if vertex.is_finite() {
260        Ok(vertex)
261    } else {
262        Err(PlyError::NonFiniteVertex)
263    }
264}
265
266fn read_triangle(bytes: &[u8]) -> Result<Triangle32, PlyError> {
267    if take(bytes, 0, 1)?.first().copied() != Some(3) {
268        return Err(PlyError::NonTriangularFace);
269    }
270    Ok(Triangle32::new(
271        read_index(bytes, 1)?,
272        read_index(bytes, 5)?,
273        read_index(bytes, 9)?,
274    ))
275}
276
277fn read_index(bytes: &[u8], start: usize) -> Result<u32, PlyError> {
278    let value = i32::from_le_bytes(take_array(bytes, start)?);
279    u32::try_from(value).map_err(|_| PlyError::NegativeIndex)
280}
281
282fn validate_triangle_bounds(triangle: Triangle32, vertex_count: usize) -> Result<(), PlyError> {
283    if index_is_valid(triangle.v0, vertex_count)
284        && index_is_valid(triangle.v1, vertex_count)
285        && index_is_valid(triangle.v2, vertex_count)
286    {
287        Ok(())
288    } else {
289        Err(PlyError::IndexOutOfBounds)
290    }
291}
292
293fn index_is_valid(index: u32, vertex_count: usize) -> bool {
294    usize::try_from(index).is_ok_and(|index| index < vertex_count)
295}