Skip to main content

ogeom_io/
ply.rs

1//! PLY in all three of its encodings, for meshes and for point clouds.
2//!
3//! A PLY file is a text header declaring elements, each a count and a list
4//! of typed properties, then the elements' data in that order: as text, or
5//! as packed little- or big-endian binary. A scan keeps everything a
6//! scanner measured at a vertex in it, so [`read_ply_bytes`] reads the
7//! vertex element whole into a [`PointCloud`]: the position, a normal, a
8//! colour, and every other scalar property by its name. Faces are
9//! optional; a file without them is a cloud.
10//!
11//! The header's counts are promises the body must keep, and each is held
12//! to the bytes left before anything is allocated for it, so a short or
13//! hostile file is refused by name rather than read into memory it does
14//! not back.
15
16use ogeom_core::{OgeomResult, ogeom_bail};
17use ogeom_math::{Point, Vector};
18use ogeom_topo::Triangulation;
19use std::fmt::Write as _;
20
21use crate::cloud::{PointCloud, Scalar};
22
23/// What [`read_ply_bytes`] found.
24#[derive(Debug, Clone)]
25pub struct PlyImport {
26    /// The vertex element: positions and every other property read at
27    /// each vertex, colours scaled into `[0, 1]`.
28    pub vertices: PointCloud,
29    /// The faces, each fanned from its first corner into triangles; empty
30    /// for a point cloud.
31    pub triangles: Vec<[u32; 3]>,
32    /// An RGBA colour per triangle, where the face element carries one;
33    /// a face fanned into several triangles gives each its colour.
34    pub triangle_colours: Option<Vec<[f64; 4]>>,
35    /// What was read with a caveat or passed over: elements other than
36    /// vertices and faces, list properties of a vertex, faces of fewer
37    /// than three corners.
38    pub warnings: Vec<String>,
39}
40
41impl PlyImport {
42    /// The triangles as a mesh over the vertices.
43    ///
44    /// A vertex keeps the file's normal where it has a unit one; any other
45    /// takes the area-weighted normal of the triangles around it, and a
46    /// vertex on none of them points along `+z`.
47    #[must_use]
48    pub fn mesh(&self) -> Triangulation {
49        let positions = self.vertices.points.clone();
50        let mut resolved = self
51            .vertices
52            .normals
53            .clone()
54            .unwrap_or_else(|| vec![Vector::ZERO; positions.len()]);
55        for triangle in &self.triangles {
56            let [a, b, c] = triangle.map(|i| positions[i as usize]);
57            let face = (b - a).cross(c - a);
58            for index in triangle {
59                let slot = &mut resolved[*index as usize];
60                if crate::mesh_formats::held_is_unset(slot) {
61                    *slot += face;
62                }
63            }
64        }
65        let normals = resolved
66            .into_iter()
67            .map(|n| {
68                let m = n.magnitude();
69                if m > 0.0 { n / m } else { Vector::Z }
70            })
71            .collect();
72        Triangulation {
73            parameters: vec![(0.0, 0.0); positions.len()],
74            positions,
75            normals,
76            triangles: self.triangles.clone(),
77            deflection_met: true,
78        }
79    }
80}
81
82/// A scalar type the header names.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84enum Kind {
85    I8,
86    U8,
87    I16,
88    U16,
89    I32,
90    U32,
91    F32,
92    F64,
93    /// A type the format does not define, readable only as text, where
94    /// every value is a number whatever its type.
95    Unknown,
96}
97
98impl Kind {
99    fn parse(word: &str) -> Self {
100        match word {
101            "char" | "int8" => Self::I8,
102            "uchar" | "uint8" => Self::U8,
103            "short" | "int16" => Self::I16,
104            "ushort" | "uint16" => Self::U16,
105            "int" | "int32" => Self::I32,
106            "uint" | "uint32" => Self::U32,
107            "float" | "float32" => Self::F32,
108            "double" | "float64" => Self::F64,
109            _ => Self::Unknown,
110        }
111    }
112
113    fn size(self) -> usize {
114        match self {
115            Self::I8 | Self::U8 => 1,
116            Self::I16 | Self::U16 => 2,
117            Self::I32 | Self::U32 | Self::F32 => 4,
118            Self::F64 => 8,
119            Self::Unknown => 0,
120        }
121    }
122
123    fn is_integer(self) -> bool {
124        !matches!(self, Self::F32 | Self::F64 | Self::Unknown)
125    }
126
127    /// What full intensity is for a colour channel of this type: the
128    /// type's largest value for an integer, one for a float.
129    fn full(self) -> f64 {
130        match self {
131            Self::I8 => f64::from(i8::MAX),
132            Self::U8 => f64::from(u8::MAX),
133            Self::I16 => f64::from(i16::MAX),
134            Self::U16 => f64::from(u16::MAX),
135            Self::I32 => f64::from(i32::MAX),
136            Self::U32 => f64::from(u32::MAX),
137            Self::F32 | Self::F64 | Self::Unknown => 1.0,
138        }
139    }
140}
141
142#[derive(Debug, Clone)]
143enum Property {
144    Scalar {
145        name: String,
146        kind: Kind,
147    },
148    List {
149        name: String,
150        count: Kind,
151        item: Kind,
152    },
153}
154
155impl Property {
156    fn name(&self) -> &str {
157        match self {
158            Self::Scalar { name, .. } | Self::List { name, .. } => name,
159        }
160    }
161}
162
163#[derive(Debug)]
164struct Element {
165    name: String,
166    count: usize,
167    properties: Vec<Property>,
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171enum Encoding {
172    Ascii,
173    Little,
174    Big,
175}
176
177/// The header, and where the body starts.
178fn header(bytes: &[u8]) -> OgeomResult<(Encoding, Vec<Element>, usize)> {
179    let Some(end) = bytes.windows(10).position(|w| w == b"end_header") else {
180        if bytes.starts_with(b"ply") {
181            ogeom_bail!(Construction, "the PLY header never ends");
182        }
183        ogeom_bail!(Construction, "this is not a PLY file");
184    };
185    let mut body = end + 10;
186    if bytes.get(body) == Some(&b'\r') {
187        body += 1;
188    }
189    if bytes.get(body) == Some(&b'\n') {
190        body += 1;
191    }
192    let text = String::from_utf8_lossy(&bytes[..end]);
193    let mut lines = text.lines();
194    if lines.next().map(str::trim) != Some("ply") {
195        ogeom_bail!(Construction, "this is not a PLY file");
196    }
197    let mut encoding = None;
198    let mut elements: Vec<Element> = Vec::new();
199    for line in lines {
200        let fields: Vec<&str> = line.split_whitespace().collect();
201        match fields.as_slice() {
202            ["format", kind, ..] => {
203                encoding = Some(match *kind {
204                    "ascii" => Encoding::Ascii,
205                    "binary_little_endian" => Encoding::Little,
206                    "binary_big_endian" => Encoding::Big,
207                    _ => ogeom_bail!(
208                        Construction,
209                        "the PLY format {kind} is not one the format defines"
210                    ),
211                });
212            }
213            ["element", name, count] => {
214                let Ok(count) = count.parse::<usize>() else {
215                    ogeom_bail!(Construction, "the PLY element {name} has the count {count}");
216                };
217                elements.push(Element {
218                    name: (*name).to_owned(),
219                    count,
220                    properties: Vec::new(),
221                });
222            }
223            ["property", "list", count, item, name] => {
224                // A property before any element belongs to nothing, and is
225                // passed over.
226                let Some(element) = elements.last_mut() else {
227                    continue;
228                };
229                let count = Kind::parse(count);
230                if !count.is_integer() && count != Kind::Unknown {
231                    ogeom_bail!(Construction, "the PLY list {name} is counted by a float");
232                }
233                element.properties.push(Property::List {
234                    name: (*name).to_owned(),
235                    count,
236                    item: Kind::parse(item),
237                });
238            }
239            ["property", kind, name] => {
240                let Some(element) = elements.last_mut() else {
241                    continue;
242                };
243                element.properties.push(Property::Scalar {
244                    name: (*name).to_owned(),
245                    kind: Kind::parse(kind),
246                });
247            }
248            ["element" | "property", ..] => {
249                ogeom_bail!(Construction, "the PLY header line {line:?} is malformed");
250            }
251            _ => {}
252        }
253    }
254    // A header without a format line is read as text, the one encoding
255    // whose bytes say what they are.
256    let encoding = encoding.unwrap_or(Encoding::Ascii);
257    if encoding != Encoding::Ascii {
258        for element in &elements {
259            for property in &element.properties {
260                let kinds = match property {
261                    Property::Scalar { kind, .. } => [*kind, *kind],
262                    Property::List { count, item, .. } => [*count, *item],
263                };
264                if kinds.contains(&Kind::Unknown) {
265                    ogeom_bail!(
266                        Construction,
267                        "the PLY property {} has a type the format does not define, so its size is unknown",
268                        property.name()
269                    );
270                }
271            }
272        }
273    }
274    Ok((encoding, elements, body))
275}
276
277/// The body, read one value at a time.
278struct Body<'a> {
279    bytes: &'a [u8],
280    at: usize,
281    encoding: Encoding,
282}
283
284impl Body<'_> {
285    fn remaining(&self) -> usize {
286        self.bytes.len() - self.at
287    }
288
289    fn value(&mut self, kind: Kind) -> OgeomResult<f64> {
290        if self.encoding == Encoding::Ascii {
291            while self.at < self.bytes.len() && self.bytes[self.at].is_ascii_whitespace() {
292                self.at += 1;
293            }
294            let start = self.at;
295            while self.at < self.bytes.len() && !self.bytes[self.at].is_ascii_whitespace() {
296                self.at += 1;
297            }
298            if start == self.at {
299                ogeom_bail!(
300                    Construction,
301                    "the PLY body ends before the header's counts are met"
302                );
303            }
304            let word = String::from_utf8_lossy(&self.bytes[start..self.at]);
305            let Ok(value) = word.parse::<f64>() else {
306                ogeom_bail!(
307                    Construction,
308                    "the PLY body carries {word}, which is not a number"
309                );
310            };
311            if kind.is_integer() && value.fract() != 0.0 {
312                ogeom_bail!(
313                    Construction,
314                    "the PLY body carries {word} for an integer property"
315                );
316            }
317            return Ok(value);
318        }
319        let size = kind.size();
320        let Some(raw) = self.bytes.get(self.at..self.at + size) else {
321            ogeom_bail!(
322                Construction,
323                "the PLY body ends before the header's counts are met"
324            );
325        };
326        self.at += size;
327        let mut b = [0_u8; 8];
328        b[..size].copy_from_slice(raw);
329        if self.encoding == Encoding::Big {
330            b[..size].reverse();
331        }
332        Ok(match kind {
333            Kind::I8 => f64::from(i8::from_le_bytes([b[0]])),
334            Kind::U8 => f64::from(b[0]),
335            Kind::I16 => f64::from(i16::from_le_bytes([b[0], b[1]])),
336            Kind::U16 => f64::from(u16::from_le_bytes([b[0], b[1]])),
337            Kind::I32 => f64::from(i32::from_le_bytes([b[0], b[1], b[2], b[3]])),
338            Kind::U32 => f64::from(u32::from_le_bytes([b[0], b[1], b[2], b[3]])),
339            Kind::F32 => f64::from(f32::from_le_bytes([b[0], b[1], b[2], b[3]])),
340            Kind::F64 | Kind::Unknown => f64::from_le_bytes(b),
341        })
342    }
343
344    /// A list's length, refused where the bytes left could not hold it.
345    fn length(&mut self, count: Kind, item: Kind) -> OgeomResult<usize> {
346        let value = self.value(count)?;
347        if value < 0.0 || value.fract() != 0.0 {
348            ogeom_bail!(Construction, "a PLY list has the length {value}");
349        }
350        #[allow(
351            clippy::cast_possible_truncation,
352            clippy::cast_sign_loss,
353            reason = "a non-negative integer of at most 32 bits"
354        )]
355        let n = value as usize;
356        let least = match self.encoding {
357            Encoding::Ascii => n.saturating_mul(2).saturating_sub(1),
358            _ => n.saturating_mul(item.size()),
359        };
360        if least > self.remaining() {
361            ogeom_bail!(
362                Construction,
363                "a PLY list of {n} items runs past the end of the file"
364            );
365        }
366        Ok(n)
367    }
368
369    /// Refuse an element whose count the bytes left could not hold, at
370    /// the least room each instance takes.
371    fn holds(&self, element: &Element) -> OgeomResult<()> {
372        let per: usize = element
373            .properties
374            .iter()
375            .map(|p| match (self.encoding, p) {
376                (Encoding::Ascii, _) => 2,
377                (_, Property::Scalar { kind, .. }) => kind.size(),
378                (_, Property::List { count, .. }) => count.size(),
379            })
380            .sum();
381        let least = element.count.saturating_mul(per);
382        let room = match self.encoding {
383            Encoding::Ascii => self.remaining().saturating_add(1),
384            _ => self.remaining(),
385        };
386        if least > room {
387            ogeom_bail!(
388                Construction,
389                "the PLY header promises {} of {}, more than the {} bytes left could hold",
390                element.count,
391                element.name,
392                self.remaining()
393            );
394        }
395        Ok(())
396    }
397}
398
399/// Where a colour channel sits among an element's properties, and its type.
400fn channel(properties: &[Property], names: [&str; 3]) -> Option<(usize, Kind)> {
401    properties.iter().enumerate().find_map(|(i, p)| match p {
402        Property::Scalar { name, kind } if names.contains(&name.as_str()) => Some((i, *kind)),
403        _ => None,
404    })
405}
406
407/// The colour channels of an element: red, green, blue, and alpha where
408/// it has one.
409fn colour_channels(properties: &[Property]) -> Option<[Option<(usize, Kind)>; 4]> {
410    let red = channel(properties, ["red", "r", "diffuse_red"]);
411    let green = channel(properties, ["green", "g", "diffuse_green"]);
412    let blue = channel(properties, ["blue", "b", "diffuse_blue"]);
413    let alpha = channel(properties, ["alpha", "a", "diffuse_alpha"]);
414    (red.is_some() && green.is_some() && blue.is_some()).then_some([red, green, blue, alpha])
415}
416
417fn colour_of(values: &[f64], channels: &[Option<(usize, Kind)>; 4]) -> [f64; 4] {
418    channels.map(|c| c.map_or(1.0, |(i, kind)| values[i] / kind.full()))
419}
420
421/// Read a PLY, ASCII or binary of either byte order, mesh or point cloud.
422///
423/// Every element the header declares is read in its order. Of the vertex
424/// element: `x`, `y` and `z` (required), `nx`, `ny` and `nz` as a normal,
425/// `red`, `green`, `blue` and `alpha` as a colour (also spelled `r`, `g`,
426/// `b`, `a` or `diffuse_red` and so on; an integer channel scaled by its
427/// type's largest value, a float one taken as it is), and every other
428/// scalar property as a [`Scalar`] of its name. Of the face element: the
429/// list named `vertex_indices` (or `vertex_index`, or else its first
430/// list), of any count and index type, fanned into triangles, and a colour
431/// as for vertices. Other elements and properties are passed over.
432///
433/// PLY declares no unit; coordinates come back as written.
434///
435/// # Errors
436///
437/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
438/// bytes are not a PLY file, the header is malformed or names a type the
439/// format does not define, a count promises more than the file holds, the
440/// body ends early or carries a value that does not parse, the vertices
441/// lack a coordinate or are absent, or a face names a vertex that is not
442/// there.
443pub fn read_ply_bytes(bytes: &[u8]) -> OgeomResult<PlyImport> {
444    let (encoding, elements, start) = header(bytes)?;
445    let mut body = Body {
446        bytes,
447        at: start,
448        encoding,
449    };
450    let mut vertices: Option<PointCloud> = None;
451    let mut triangles: Vec<[u32; 3]> = Vec::new();
452    let mut triangle_colours: Vec<[f64; 4]> = Vec::new();
453    let mut face_coloured = false;
454    let mut warnings = Vec::new();
455    let mut short_faces = 0_usize;
456    let mut values: Vec<f64> = Vec::new();
457    let mut corners: Vec<u32> = Vec::new();
458    for element in &elements {
459        if element.properties.is_empty() || element.count == 0 {
460            continue;
461        }
462        body.holds(element)?;
463        match element.name.as_str() {
464            "vertex" => {
465                let at = |name: &str| {
466                    element
467                        .properties
468                        .iter()
469                        .position(|p| matches!(p, Property::Scalar { name: n, .. } if n == name))
470                };
471                let (Some(x), Some(y), Some(z)) = (at("x"), at("y"), at("z")) else {
472                    ogeom_bail!(Construction, "a vertex is missing a coordinate");
473                };
474                let normal = match (at("nx"), at("ny"), at("nz")) {
475                    (Some(a), Some(b), Some(c)) => Some([a, b, c]),
476                    _ => None,
477                };
478                let colour = colour_channels(&element.properties);
479                let mut taken = vec![x, y, z];
480                taken.extend(normal.iter().flatten());
481                taken.extend(colour.iter().flatten().flatten().map(|(i, _)| *i));
482                let scalar_at: Vec<usize> = (0..element.properties.len())
483                    .filter(|i| {
484                        !taken.contains(i)
485                            && matches!(element.properties[*i], Property::Scalar { .. })
486                    })
487                    .collect();
488                if element
489                    .properties
490                    .iter()
491                    .any(|p| matches!(p, Property::List { .. }))
492                {
493                    warnings
494                        .push("the vertices carry list properties, which are not read".to_owned());
495                }
496                let mut cloud = PointCloud {
497                    points: Vec::with_capacity(element.count),
498                    normals: normal.map(|_| Vec::with_capacity(element.count)),
499                    colours: colour.map(|_| Vec::with_capacity(element.count)),
500                    scalars: scalar_at
501                        .iter()
502                        .map(|&i| Scalar {
503                            name: element.properties[i].name().to_owned(),
504                            values: Vec::with_capacity(element.count),
505                        })
506                        .collect(),
507                };
508                for _ in 0..element.count {
509                    read_instance(&mut body, &element.properties, &mut values, None)?;
510                    cloud
511                        .points
512                        .push(Point::new(values[x], values[y], values[z]));
513                    if let (Some(normals), Some([a, b, c])) = (cloud.normals.as_mut(), normal) {
514                        normals.push(Vector::new(values[a], values[b], values[c]));
515                    }
516                    if let (Some(colours), Some(channels)) = (cloud.colours.as_mut(), &colour) {
517                        colours.push(colour_of(&values, channels));
518                    }
519                    for (scalar, &i) in cloud.scalars.iter_mut().zip(&scalar_at) {
520                        scalar.values.push(values[i]);
521                    }
522                }
523                if vertices.is_some() {
524                    ogeom_bail!(Construction, "the PLY declares its vertices twice");
525                }
526                vertices = Some(cloud);
527            }
528            "face" => {
529                let lists = || {
530                    element
531                        .properties
532                        .iter()
533                        .enumerate()
534                        .filter(|(_, p)| matches!(p, Property::List { .. }))
535                };
536                let indices = lists()
537                    .find(|(_, p)| matches!(p.name(), "vertex_indices" | "vertex_index"))
538                    .or_else(|| lists().next())
539                    .map(|(i, _)| i);
540                let Some(indices) = indices else {
541                    ogeom_bail!(Construction, "the PLY faces carry no list of vertices");
542                };
543                let colour = colour_channels(&element.properties);
544                face_coloured = colour.is_some();
545                for _ in 0..element.count {
546                    read_instance(
547                        &mut body,
548                        &element.properties,
549                        &mut values,
550                        Some((indices, &mut corners)),
551                    )?;
552                    if corners.len() < 3 {
553                        short_faces += 1;
554                        continue;
555                    }
556                    for i in 1..corners.len() - 1 {
557                        triangles.push([corners[0], corners[i], corners[i + 1]]);
558                        if let Some(channels) = &colour {
559                            triangle_colours.push(colour_of(&values, channels));
560                        }
561                    }
562                }
563            }
564            other => {
565                warnings.push(format!("the element {other} is not read"));
566                for _ in 0..element.count {
567                    read_instance(&mut body, &element.properties, &mut values, None)?;
568                }
569            }
570        }
571    }
572    let Some(vertices) = vertices.filter(|v| !v.is_empty()) else {
573        ogeom_bail!(Construction, "the file carries no vertices");
574    };
575    if let Some(past) = triangles
576        .iter()
577        .flatten()
578        .find(|&&v| v as usize >= vertices.len())
579    {
580        ogeom_bail!(
581            Construction,
582            "a face names vertex {past}, past the {} the file has",
583            vertices.len()
584        );
585    }
586    if short_faces > 0 {
587        warnings.push(format!(
588            "{short_faces} faces of fewer than three corners were passed over"
589        ));
590    }
591    Ok(PlyImport {
592        vertices,
593        triangles,
594        triangle_colours: face_coloured.then_some(triangle_colours),
595        warnings,
596    })
597}
598
599/// Read one instance of an element: its scalars into `values` at their
600/// property positions (a list's slot left at zero), and the list at
601/// `corners.0`, if asked for, into `corners.1` as vertex indices.
602fn read_instance(
603    body: &mut Body<'_>,
604    properties: &[Property],
605    values: &mut Vec<f64>,
606    mut corners: Option<(usize, &mut Vec<u32>)>,
607) -> OgeomResult<()> {
608    values.clear();
609    for (k, property) in properties.iter().enumerate() {
610        match property {
611            Property::Scalar { kind, .. } => values.push(body.value(*kind)?),
612            Property::List { count, item, .. } => {
613                values.push(0.0);
614                let n = body.length(*count, *item)?;
615                match corners.as_mut() {
616                    Some((at, out)) if *at == k => {
617                        out.clear();
618                        for _ in 0..n {
619                            let v = body.value(*item)?;
620                            if v < 0.0 || v > f64::from(u32::MAX) || v.fract() != 0.0 {
621                                ogeom_bail!(Construction, "a face names vertex {v}");
622                            }
623                            #[allow(
624                                clippy::cast_possible_truncation,
625                                clippy::cast_sign_loss,
626                                reason = "an integer within u32, checked above"
627                            )]
628                            out.push(v as u32);
629                        }
630                    }
631                    _ => {
632                        for _ in 0..n {
633                            body.value(*item)?;
634                        }
635                    }
636                }
637            }
638        }
639    }
640    Ok(())
641}
642
643/// The property names the writer gives the position, normal and colour.
644const RESERVED: [&str; 10] = [
645    "x", "y", "z", "nx", "ny", "nz", "red", "green", "blue", "alpha",
646];
647
648/// Write a cloud, or a mesh over it, as binary little-endian PLY.
649///
650/// Positions as `double`, so they come back as the same `f64`; normals as
651/// `float`; colours as `uchar` channels, alpha only where some point is
652/// not opaque; each scalar as a `double` property of its name. With
653/// triangles, a face element of `uchar`-counted `int` index lists follows;
654/// without, the file is a point cloud. [`read_ply_bytes`] reads it back.
655///
656/// # Errors
657///
658/// [`OgeomError::Dimension`](ogeom_core::OgeomError::Dimension) if an
659/// attribute does not have one entry per point (see [`PointCloud::check`]);
660/// [`OgeomError::Range`](ogeom_core::OgeomError::Range) for a triangle
661/// naming a vertex that is not there, or past the `int` range;
662/// [`OgeomError::Domain`](ogeom_core::OgeomError::Domain) for a scalar name
663/// that is empty, holds white space, or is one the writer gives the
664/// position, normal or colour.
665pub fn write_ply_binary(vertices: &PointCloud, triangles: &[[u32; 3]]) -> OgeomResult<Vec<u8>> {
666    vertices.check()?;
667    let n = vertices.len();
668    if let Some(past) = triangles
669        .iter()
670        .flatten()
671        .find(|&&v| v as usize >= n || i32::try_from(v).is_err())
672    {
673        ogeom_bail!(Range, "a triangle names vertex {past} of a cloud of {n}");
674    }
675    for s in &vertices.scalars {
676        if s.name.is_empty()
677            || s.name.chars().any(char::is_whitespace)
678            || RESERVED.contains(&s.name.as_str())
679        {
680            ogeom_bail!(
681                Domain,
682                "the scalar name {:?} cannot name a PLY property",
683                s.name
684            );
685        }
686    }
687    let alpha = vertices
688        .colours
689        .as_ref()
690        .is_some_and(|c| c.iter().any(|c| c[3] < 1.0));
691    let mut head = String::from("ply\nformat binary_little_endian 1.0\ncomment ogeom\n");
692    let _ = writeln!(head, "element vertex {n}");
693    head.push_str("property double x\nproperty double y\nproperty double z\n");
694    if vertices.normals.is_some() {
695        head.push_str("property float nx\nproperty float ny\nproperty float nz\n");
696    }
697    if vertices.colours.is_some() {
698        head.push_str("property uchar red\nproperty uchar green\nproperty uchar blue\n");
699        if alpha {
700            head.push_str("property uchar alpha\n");
701        }
702    }
703    for s in &vertices.scalars {
704        let _ = writeln!(head, "property double {}", s.name);
705    }
706    if !triangles.is_empty() {
707        let _ = writeln!(head, "element face {}", triangles.len());
708        head.push_str("property list uchar int vertex_indices\n");
709    }
710    head.push_str("end_header\n");
711    let mut out = head.into_bytes();
712    for (i, p) in vertices.points.iter().enumerate() {
713        for c in [p.x, p.y, p.z] {
714            out.extend_from_slice(&c.to_le_bytes());
715        }
716        if let Some(normals) = &vertices.normals {
717            let v = normals[i];
718            for c in [v.x, v.y, v.z] {
719                #[allow(clippy::cast_possible_truncation, reason = "the file's float")]
720                out.extend_from_slice(&(c as f32).to_le_bytes());
721            }
722        }
723        if let Some(colours) = &vertices.colours {
724            let channels = if alpha { 4 } else { 3 };
725            for &c in &colours[i][..channels] {
726                out.push(crate::cloud::channel_byte(c));
727            }
728        }
729        for s in &vertices.scalars {
730            out.extend_from_slice(&s.values[i].to_le_bytes());
731        }
732    }
733    for t in triangles {
734        out.push(3);
735        for &v in t {
736            out.extend_from_slice(&v.to_le_bytes());
737        }
738    }
739    Ok(out)
740}
741
742#[cfg(test)]
743mod tests {
744    #![allow(
745        clippy::unwrap_used,
746        clippy::cast_possible_truncation,
747        reason = "test code"
748    )]
749    use super::*;
750
751    fn tetrahedron_cloud() -> PointCloud {
752        PointCloud {
753            points: vec![
754                Point::new(0.0, 0.0, 0.0),
755                Point::new(1.25, 0.0, 0.0),
756                Point::new(0.0, 1.0, 0.0),
757                Point::new(0.0, 0.0, 0.1),
758            ],
759            normals: Some(vec![
760                Vector::new(-0.6, 0.0, -0.8),
761                Vector::X,
762                Vector::Y,
763                Vector::Z,
764            ]),
765            colours: Some(vec![
766                [1.0, 0.0, 0.0, 1.0],
767                [0.0, 1.0, 0.0, 1.0],
768                [0.0, 0.0, 1.0, 51.0 / 255.0],
769                [1.0, 1.0, 1.0, 1.0],
770            ]),
771            scalars: vec![Scalar {
772                name: "confidence".into(),
773                values: vec![0.1, 0.2, 0.3, 1e-300],
774            }],
775        }
776    }
777
778    const TETRA: [[u32; 3]; 4] = [[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]];
779
780    #[test]
781    fn binary_ply_round_trips_a_mesh_and_a_cloud() {
782        let cloud = tetrahedron_cloud();
783        let bytes = write_ply_binary(&cloud, &TETRA).unwrap();
784        let back = read_ply_bytes(&bytes).unwrap();
785        assert!(back.warnings.is_empty(), "{:?}", back.warnings);
786        assert_eq!(back.triangles, TETRA);
787        assert!(back.triangle_colours.is_none());
788        let mut expected = cloud.clone();
789        // Normals travel as float.
790        expected.normals = cloud.normals.as_ref().map(|n| {
791            n.iter()
792                .map(|v| {
793                    Vector::new(
794                        f64::from(v.x as f32),
795                        f64::from(v.y as f32),
796                        f64::from(v.z as f32),
797                    )
798                })
799                .collect()
800        });
801        assert_eq!(back.vertices, expected);
802
803        let points = write_ply_binary(&cloud, &[]).unwrap();
804        let back = read_ply_bytes(&points).unwrap();
805        assert!(back.triangles.is_empty());
806        assert_eq!(back.vertices.points, cloud.points);
807        assert!(!String::from_utf8_lossy(&points).contains("element face"));
808    }
809
810    /// The same mesh spelled three ways (text, little-endian with `ushort`
811    /// counts and `uint` indices, big-endian with `char` counts and
812    /// `short` indices) reads to the same vertices and triangles, a quad
813    /// fanned into two of them, each with the face's colour.
814    #[test]
815    fn every_encoding_and_list_type_reads_alike() {
816        let ascii = b"ply\nformat ascii 1.0\nelement vertex 4\nproperty float x\nproperty float y\n\
817property float z\nproperty uchar red\nproperty uchar green\nproperty uchar blue\nproperty float quality\n\
818element face 1\nproperty list uchar int vertex_indices\nproperty float red\nproperty float green\n\
819property float blue\nend_header\n0 0 0 255 0 0 0.5\n1 0 0 0 255 0 1.5\n1 1 0 0 0 255 2.5\n0 1 0 0 0 0 3.5\n\
8204 0 1 2 3 0.25 0.5 1\n";
821        let reference = read_ply_bytes(ascii).unwrap();
822        assert_eq!(reference.triangles, vec![[0, 1, 2], [0, 2, 3]]);
823        assert_eq!(
824            reference.triangle_colours,
825            Some(vec![[0.25, 0.5, 1.0, 1.0]; 2])
826        );
827        assert_eq!(
828            reference.vertices.scalar("quality"),
829            Some(&[0.5, 1.5, 2.5, 3.5][..])
830        );
831        assert_eq!(
832            reference.vertices.colours.as_ref().unwrap()[2],
833            [0.0, 0.0, 1.0, 1.0]
834        );
835
836        for big in [false, true] {
837            let (count, index) = if big {
838                ("char", "short")
839            } else {
840                ("ushort", "uint")
841            };
842            let mut bytes = format!(
843                "ply\nformat {} 1.0\nelement vertex 4\nproperty float x\nproperty float y\n\
844property float z\nproperty uchar red\nproperty uchar green\nproperty uchar blue\nproperty float quality\n\
845element face 1\nproperty list {count} {index} vertex_indices\nproperty float red\nproperty float green\n\
846property float blue\nend_header\n",
847                if big { "binary_big_endian" } else { "binary_little_endian" }
848            )
849            .into_bytes();
850            let f = |out: &mut Vec<u8>, v: f32| {
851                out.extend_from_slice(&if big {
852                    v.to_be_bytes()
853                } else {
854                    v.to_le_bytes()
855                });
856            };
857            let vertices = [
858                ([0.0, 0.0, 0.0], [255, 0, 0], 0.5),
859                ([1.0, 0.0, 0.0], [0, 255, 0], 1.5),
860                ([1.0, 1.0, 0.0], [0, 0, 255], 2.5),
861                ([0.0, 1.0, 0.0], [0, 0, 0], 3.5),
862            ];
863            for (p, c, q) in vertices {
864                for v in p {
865                    f(&mut bytes, v);
866                }
867                bytes.extend_from_slice(&c);
868                f(&mut bytes, q);
869            }
870            if big {
871                bytes.push(4);
872                for i in 0..4_i16 {
873                    bytes.extend_from_slice(&i.to_be_bytes());
874                }
875            } else {
876                bytes.extend_from_slice(&4_u16.to_le_bytes());
877                for i in 0..4_u32 {
878                    bytes.extend_from_slice(&i.to_le_bytes());
879                }
880            }
881            for v in [0.25, 0.5, 1.0] {
882                f(&mut bytes, v);
883            }
884            let read = read_ply_bytes(&bytes).unwrap();
885            assert_eq!(read.triangles, reference.triangles, "big endian: {big}");
886            assert_eq!(read.triangle_colours, reference.triangle_colours);
887            assert_eq!(read.vertices, reference.vertices);
888        }
889    }
890
891    #[test]
892    fn truncated_and_hostile_ply_is_refused_by_name() {
893        let bytes = write_ply_binary(&tetrahedron_cloud(), &TETRA).unwrap();
894        for cut in 0..bytes.len() {
895            assert!(read_ply_bytes(&bytes[..cut]).is_err(), "cut at {cut}");
896        }
897        // A count no file of this size could back is refused before any
898        // allocation is made for it.
899        let huge = b"ply\nformat binary_little_endian 1.0\nelement vertex 4000000000000\n\
900property double x\nproperty double y\nproperty double z\nend_header\n\0\0\0\0";
901        let error = read_ply_bytes(huge).unwrap_err().to_string();
902        assert!(error.contains("promises"), "{error}");
903        let long_list = b"ply\nformat binary_little_endian 1.0\nelement vertex 1\nproperty uchar x\n\
904property uchar y\nproperty uchar z\nelement face 1\nproperty list uint int vertex_indices\nend_header\n\
905\0\0\0\xff\xff\xff\xff";
906        assert!(read_ply_bytes(long_list).is_err());
907        for bad in [
908            &b"ply\nformat binary_little_endian 1.0\nelement vertex 1\nproperty quad x\nend_header\n0\n"[..],
909            b"ply\nformat zip 1.0\nend_header\n",
910            b"ply\nelement vertex 1\nproperty float x\nend_header\n0\n",
911            b"ply\nformat ascii 1.0\nproperty float x\nend_header\n",
912            b"ply\nformat ascii 1.0\nelement vertex x\nproperty float x\nend_header\n",
913            b"ply\nformat ascii 1.0\nelement vertex 1\nproperty float x\nend_header\n1\n",
914            b"ply\nformat ascii 1.0\nelement vertex 1\nproperty float x\nproperty float y\n\
915property float z\nelement face 1\nproperty list uchar int vertex_indices\nend_header\n0 0 0\n3 0 1 -2\n",
916            b"\x00\x01garbage end_header",
917            b"",
918        ] {
919            assert!(read_ply_bytes(bad).is_err(), "{}", String::from_utf8_lossy(bad));
920        }
921        // A type the format does not define is read as text, where a value
922        // is a number whatever its type.
923        let wide = read_ply_bytes(
924            b"ply\nformat ascii 1.0\nproperty float stray\nelement vertex 1\nproperty int64 x\n\
925property int64 y\nproperty int64 z\nend_header\n1 2 3\n",
926        )
927        .unwrap();
928        assert_eq!(wide.vertices.points, vec![Point::new(1.0, 2.0, 3.0)]);
929        let mut seed = 0x2545_f491_u32;
930        for _ in 0..2000 {
931            let mut noisy = bytes.clone();
932            for _ in 0..4 {
933                seed ^= seed << 13;
934                seed ^= seed >> 17;
935                seed ^= seed << 5;
936                let at = seed as usize % noisy.len();
937                noisy[at] = (seed >> 8) as u8;
938            }
939            let _ = read_ply_bytes(&noisy);
940        }
941    }
942
943    #[test]
944    fn the_writer_refuses_what_it_cannot_say() {
945        let cloud = tetrahedron_cloud();
946        assert!(write_ply_binary(&cloud, &[[0, 1, 4]]).is_err());
947        let mut named = cloud.clone();
948        named.scalars[0].name = "nx".into();
949        assert!(write_ply_binary(&named, &[]).is_err());
950        named.scalars[0].name = "two words".into();
951        assert!(write_ply_binary(&named, &[]).is_err());
952        let mut short = cloud;
953        short.colours.as_mut().unwrap().pop();
954        assert!(write_ply_binary(&short, &[]).is_err());
955    }
956}