Skip to main content

draco_io/
stl_reader.rs

1//! STL format reader for triangle meshes.
2//!
3//! Reads both containers the format is written in — binary and ASCII — and
4//! produces a mesh in the shape STL states it: three vertices per triangle,
5//! shared by nothing, with the facet normal replicated onto each of them. STL
6//! carries no vertex identity, so welding would be this reader inventing one.
7
8use std::fs;
9use std::io::{self, Cursor, Read};
10use std::path::Path;
11
12use draco_core::draco_types::DataType;
13use draco_core::geometry_attribute::{GeometryAttributeType, PointAttribute};
14use draco_core::geometry_indices::{FaceIndex, PointIndex};
15use draco_core::mesh::Mesh;
16
17use crate::traits::{ReadFromBytes, Reader};
18
19/// The fixed part of a binary STL: an 80-byte header and the triangle count.
20const BINARY_HEADER_LENGTH: usize = 84;
21/// Three float triples, a normal, and the two-byte attribute count.
22const BINARY_TRIANGLE_LENGTH: usize = 50;
23
24/// STL format reader.
25#[derive(Debug)]
26pub struct StlReader {
27    source: StlReaderSource,
28}
29
30#[derive(Debug, Clone)]
31enum StlReaderSource {
32    Path(std::path::PathBuf),
33    Bytes(Vec<u8>),
34}
35
36/// One triangle as the file states it: a facet normal and three corners.
37#[derive(Debug, Clone, Copy, PartialEq)]
38struct StlTriangle {
39    normal: [f32; 3],
40    vertices: [[f32; 3]; 3],
41}
42
43impl StlReader {
44    /// Open an STL file for reading.
45    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
46        let path = path.as_ref().to_path_buf();
47        if !path.exists() {
48            return Err(io::Error::new(
49                io::ErrorKind::NotFound,
50                format!("File not found: {}", path.display()),
51            ));
52        }
53        Ok(Self {
54            source: StlReaderSource::Path(path),
55        })
56    }
57
58    /// Create an STL reader from in-memory bytes.
59    pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
60        Self {
61            source: StlReaderSource::Bytes(bytes.into()),
62        }
63    }
64
65    /// Read a mesh directly from in-memory bytes.
66    pub fn read_from_bytes(bytes: &[u8]) -> io::Result<Mesh> {
67        let mut reader = Self::from_bytes(bytes.to_vec());
68        reader.read_mesh()
69    }
70
71    /// Read a mesh from the STL file.
72    pub fn read_mesh(&mut self) -> io::Result<Mesh> {
73        let bytes = match &self.source {
74            StlReaderSource::Path(path) => fs::read(path)?,
75            StlReaderSource::Bytes(bytes) => bytes.clone(),
76        };
77        Ok(triangles_to_mesh(&read_stl_bytes(&bytes)?))
78    }
79}
80
81impl Reader for StlReader {
82    fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
83        StlReader::open(path)
84    }
85
86    fn read_meshes(&mut self) -> io::Result<Vec<Mesh>> {
87        Ok(vec![self.read_mesh()?])
88    }
89}
90
91impl ReadFromBytes for StlReader {
92    fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
93        Ok(Self::from_bytes(bytes.to_vec()))
94    }
95}
96
97fn invalid_stl(message: &str) -> io::Error {
98    io::Error::new(io::ErrorKind::InvalidData, message.to_string())
99}
100
101/// Whether these bytes are a binary STL rather than an ASCII one.
102///
103/// The leading keyword decides nothing: exporters exist that write `solid` into
104/// the 80-byte header of a binary file, so a reader trusting it reads a binary
105/// mesh as text and finds no facets at all. The length does decide — a binary
106/// file is exactly 84 bytes plus 50 per triangle, and that is checked first.
107fn is_binary_stl(bytes: &[u8]) -> bool {
108    if bytes.len() < BINARY_HEADER_LENGTH {
109        return false;
110    }
111    let count = u32::from_le_bytes(bytes[80..84].try_into().unwrap()) as usize;
112    if let Some(expected) = count
113        .checked_mul(BINARY_TRIANGLE_LENGTH)
114        .and_then(|body| body.checked_add(BINARY_HEADER_LENGTH))
115    {
116        if bytes.len() == expected {
117            return true;
118        }
119    }
120    // Length disagreed, so the file is truncated, padded, or text. Only then is
121    // the keyword worth asking about, and only to choose which parser reports it.
122    !bytes.starts_with(b"solid")
123}
124
125fn read_stl_bytes(bytes: &[u8]) -> io::Result<Vec<StlTriangle>> {
126    if is_binary_stl(bytes) {
127        read_binary_stl(bytes)
128    } else {
129        read_ascii_stl(
130            std::str::from_utf8(bytes)
131                .map_err(|_| invalid_stl("STL is neither a valid binary file nor UTF-8 text"))?,
132        )
133    }
134}
135
136fn read_binary_stl(bytes: &[u8]) -> io::Result<Vec<StlTriangle>> {
137    if bytes.len() < BINARY_HEADER_LENGTH {
138        return Err(invalid_stl("Binary STL is shorter than its header"));
139    }
140    let declared = u32::from_le_bytes(bytes[80..84].try_into().unwrap()) as usize;
141    // The count is read from the file, so it is a claim rather than a fact: it
142    // sizes nothing until the bytes to back it have been counted.
143    let available = (bytes.len() - BINARY_HEADER_LENGTH) / BINARY_TRIANGLE_LENGTH;
144    if declared > available {
145        return Err(invalid_stl(
146            "Binary STL declares more triangles than it contains",
147        ));
148    }
149
150    let mut cursor = Cursor::new(&bytes[BINARY_HEADER_LENGTH..]);
151    let mut triangles = Vec::with_capacity(declared);
152    let mut record = [0u8; BINARY_TRIANGLE_LENGTH];
153    for _ in 0..declared {
154        cursor.read_exact(&mut record)?;
155        let value =
156            |index: usize| f32::from_le_bytes(record[index * 4..index * 4 + 4].try_into().unwrap());
157        triangles.push(StlTriangle {
158            normal: [value(0), value(1), value(2)],
159            vertices: [
160                [value(3), value(4), value(5)],
161                [value(6), value(7), value(8)],
162                [value(9), value(10), value(11)],
163            ],
164        });
165    }
166    Ok(triangles)
167}
168
169fn read_ascii_stl(text: &str) -> io::Result<Vec<StlTriangle>> {
170    let mut triangles = Vec::new();
171    let mut normal = [0.0f32; 3];
172    let mut corners: Vec<[f32; 3]> = Vec::with_capacity(3);
173
174    for line in text.lines() {
175        let mut tokens = line.split_whitespace();
176        let keyword = match tokens.next() {
177            Some(keyword) => keyword,
178            None => continue,
179        };
180        match keyword {
181            // `facet normal nx ny nz`, and a facet whose normal is stated as
182            // anything else is taken as unstated rather than refused.
183            "facet" => {
184                corners.clear();
185                normal = read_ascii_triple(tokens.skip(1)).unwrap_or([0.0; 3]);
186            }
187            "vertex" => {
188                let vertex = read_ascii_triple(tokens)
189                    .ok_or_else(|| invalid_stl("ASCII STL vertex needs three coordinates"))?;
190                corners.push(vertex);
191            }
192            "endfacet" => {
193                // A polygon larger than a triangle is not STL, but a facet cut
194                // short is a truncated file and is worth naming as one.
195                if corners.len() < 3 {
196                    return Err(invalid_stl("ASCII STL facet has fewer than three vertices"));
197                }
198                for corner in 1..corners.len() - 1 {
199                    triangles.push(StlTriangle {
200                        normal,
201                        vertices: [corners[0], corners[corner], corners[corner + 1]],
202                    });
203                }
204                corners.clear();
205            }
206            _ => {}
207        }
208    }
209
210    Ok(triangles)
211}
212
213fn read_ascii_triple<'a>(tokens: impl Iterator<Item = &'a str>) -> Option<[f32; 3]> {
214    let values: Vec<f32> = tokens
215        .take(3)
216        .filter_map(|token| token.parse().ok())
217        .collect();
218    match values.as_slice() {
219        [x, y, z] => Some([*x, *y, *z]),
220        _ => None,
221    }
222}
223
224fn triangles_to_mesh(triangles: &[StlTriangle]) -> Mesh {
225    let mut mesh = Mesh::new();
226    if triangles.is_empty() {
227        return mesh;
228    }
229
230    let point_count = triangles.len() * 3;
231    mesh.set_num_points(point_count);
232    mesh.set_num_faces(triangles.len());
233
234    let mut positions = Vec::with_capacity(point_count);
235    let mut normals = Vec::with_capacity(point_count);
236    for triangle in triangles {
237        for vertex in triangle.vertices {
238            positions.push(vertex);
239            // STL states one normal per facet; the mesh model has none but the
240            // per-point kind, so it is written onto all three corners. Flat
241            // shading is what the format means, and this is how it survives.
242            normals.push(triangle.normal);
243        }
244    }
245
246    mesh.add_attribute(make_f32x3_attribute(
247        GeometryAttributeType::Position,
248        &positions,
249    ));
250    if normals.iter().any(|normal| normal != &[0.0, 0.0, 0.0]) {
251        mesh.add_attribute(make_f32x3_attribute(
252            GeometryAttributeType::Normal,
253            &normals,
254        ));
255    }
256
257    for (index, _) in triangles.iter().enumerate() {
258        let base = (index * 3) as u32;
259        mesh.set_face(
260            FaceIndex(index as u32),
261            [PointIndex(base), PointIndex(base + 1), PointIndex(base + 2)],
262        );
263    }
264
265    mesh
266}
267
268fn make_f32x3_attribute(
269    attribute_type: GeometryAttributeType,
270    values: &[[f32; 3]],
271) -> PointAttribute {
272    let mut attribute = PointAttribute::new();
273    attribute.init(attribute_type, 3, DataType::Float32, false, values.len());
274    let buffer = attribute.buffer_mut();
275    for (index, value) in values.iter().enumerate() {
276        let bytes: Vec<u8> = value
277            .iter()
278            .flat_map(|component| component.to_le_bytes())
279            .collect();
280        buffer.write(index * 12, &bytes);
281    }
282    attribute
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    fn binary_stl(header: &[u8], triangles: &[StlTriangle]) -> Vec<u8> {
290        let mut bytes = vec![0u8; 80];
291        bytes[..header.len().min(80)].copy_from_slice(&header[..header.len().min(80)]);
292        bytes.extend_from_slice(&(triangles.len() as u32).to_le_bytes());
293        for triangle in triangles {
294            for component in triangle.normal {
295                bytes.extend_from_slice(&component.to_le_bytes());
296            }
297            for vertex in triangle.vertices {
298                for component in vertex {
299                    bytes.extend_from_slice(&component.to_le_bytes());
300                }
301            }
302            bytes.extend_from_slice(&0u16.to_le_bytes());
303        }
304        bytes
305    }
306
307    fn one_triangle() -> StlTriangle {
308        StlTriangle {
309            normal: [0.0, 0.0, 1.0],
310            vertices: [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
311        }
312    }
313
314    #[test]
315    fn test_read_binary_stl() {
316        let data = binary_stl(b"binary", &[one_triangle()]);
317        let triangles = read_stl_bytes(&data).unwrap();
318        assert_eq!(triangles, vec![one_triangle()]);
319
320        let mesh = StlReader::read_from_bytes(&data).unwrap();
321        assert_eq!(mesh.num_points(), 3);
322        assert_eq!(mesh.num_faces(), 1);
323        assert_eq!(
324            mesh.face(FaceIndex(0)),
325            [PointIndex(0), PointIndex(1), PointIndex(2)]
326        );
327        assert!(mesh.named_attribute_id(GeometryAttributeType::Normal) >= 0);
328    }
329
330    /// The header is 80 free bytes, and exporters have put `solid` in them. A
331    /// reader that decides on the keyword reads such a file as text and finds
332    /// nothing; the length is what actually separates the two containers.
333    #[test]
334    fn test_binary_stl_whose_header_says_solid() {
335        let data = binary_stl(
336            b"solid created by an exporter that means binary",
337            &[one_triangle()],
338        );
339        assert!(is_binary_stl(&data));
340        assert_eq!(read_stl_bytes(&data).unwrap(), vec![one_triangle()]);
341    }
342
343    #[test]
344    fn test_read_ascii_stl() {
345        let text = "solid demo\n\
346             facet normal 0 0 1\n\
347             outer loop\n\
348             vertex 0 0 0\n\
349             vertex 1 0 0\n\
350             vertex 0 1 0\n\
351             endloop\n\
352             endfacet\n\
353             endsolid demo\n";
354        let triangles = read_stl_bytes(text.as_bytes()).unwrap();
355        assert_eq!(triangles, vec![one_triangle()]);
356    }
357
358    #[test]
359    fn test_ascii_stl_rejects_a_truncated_facet() {
360        let text = "solid demo\nfacet normal 0 0 1\nouter loop\nvertex 0 0 0\nendloop\nendfacet\n";
361        let error = read_stl_bytes(text.as_bytes()).unwrap_err();
362        assert_eq!(error.kind(), io::ErrorKind::InvalidData);
363    }
364
365    /// A declared count larger than the file is the one field an attacker or a
366    /// truncating transfer controls, and it used to size the allocation.
367    #[test]
368    fn test_binary_stl_rejects_an_overstated_count() {
369        let mut data = binary_stl(b"binary", &[one_triangle()]);
370        data[80..84].copy_from_slice(&1_000_000u32.to_le_bytes());
371        let error = read_stl_bytes(&data).unwrap_err();
372        assert_eq!(error.kind(), io::ErrorKind::InvalidData);
373    }
374
375    #[test]
376    fn test_empty_stl_reads_as_an_empty_mesh() {
377        let mesh = StlReader::read_from_bytes(&binary_stl(b"binary", &[])).unwrap();
378        assert_eq!(mesh.num_points(), 0);
379        assert_eq!(mesh.num_faces(), 0);
380    }
381}