Skip to main content

spatialrust_io/ply/
header.rs

1use std::io::BufRead;
2
3use crate::error::{ply_format, ply_parse, IoError};
4
5/// PLY payload encoding.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum PlyFormat {
8    /// ASCII PLY.
9    Ascii,
10    /// Binary little-endian PLY.
11    BinaryLittleEndian,
12}
13
14/// Supported PLY scalar property types.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum PlyPropertyKind {
17    /// Signed 8-bit integer.
18    Char,
19    /// Unsigned 8-bit integer.
20    UChar,
21    /// Signed 16-bit integer.
22    Short,
23    /// Unsigned 16-bit integer.
24    UShort,
25    /// Signed 32-bit integer.
26    Int,
27    /// Unsigned 32-bit integer.
28    UInt,
29    /// 32-bit float.
30    Float,
31    /// 64-bit float.
32    Double,
33}
34
35impl PlyPropertyKind {
36    /// Returns the size of one scalar value in bytes.
37    #[must_use]
38    pub const fn size_bytes(self) -> usize {
39        match self {
40            Self::Char | Self::UChar => 1,
41            Self::Short | Self::UShort => 2,
42            Self::Int | Self::UInt | Self::Float => 4,
43            Self::Double => 8,
44        }
45    }
46
47    fn parse(token: &str) -> Result<Self, IoError> {
48        match token {
49            "char" | "int8" => Ok(Self::Char),
50            "uchar" | "uint8" => Ok(Self::UChar),
51            "short" | "int16" => Ok(Self::Short),
52            "ushort" | "uint16" => Ok(Self::UShort),
53            "int" | "int32" => Ok(Self::Int),
54            "uint" | "uint32" => Ok(Self::UInt),
55            "float" | "float32" => Ok(Self::Float),
56            "double" | "float64" => Ok(Self::Double),
57            _ => Err(ply_parse(format!("unsupported PLY property type `{token}`"))),
58        }
59    }
60
61    fn as_token(self) -> &'static str {
62        match self {
63            Self::Char => "char",
64            Self::UChar => "uchar",
65            Self::Short => "short",
66            Self::UShort => "ushort",
67            Self::Int => "int",
68            Self::UInt => "uint",
69            Self::Float => "float",
70            Self::Double => "double",
71        }
72    }
73}
74
75/// One vertex property in a PLY header.
76#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct PlyProperty {
78    /// Property name.
79    pub name: String,
80    /// Scalar type.
81    pub kind: PlyPropertyKind,
82}
83
84/// Parsed PLY header for vertex-only point clouds.
85#[derive(Clone, Debug, PartialEq, Eq)]
86pub struct PlyHeader {
87    /// File encoding.
88    pub format: PlyFormat,
89    /// Number of vertex records.
90    pub vertex_count: usize,
91    /// Vertex properties in declaration order.
92    pub properties: Vec<PlyProperty>,
93}
94
95impl PlyHeader {
96    /// Parses a PLY header from a buffered reader.
97    pub fn parse<R: BufRead>(reader: &mut R) -> Result<Self, IoError> {
98        let first = read_non_empty_line(reader)?;
99        if first != "ply" {
100            return Err(ply_format(format!("expected PLY magic, found `{first}`")));
101        }
102
103        let mut format = None;
104        let mut vertex_count = None;
105        let mut properties = Vec::new();
106        let mut current_element: Option<String> = None;
107
108        loop {
109            let line = read_non_empty_line(reader)?;
110            if line == "end_header" {
111                break;
112            }
113
114            let mut parts = line.split_whitespace();
115            let keyword = parts.next().ok_or_else(|| ply_parse("empty PLY header line"))?;
116            match keyword {
117                "format" => {
118                    let token =
119                        parts.next().ok_or_else(|| ply_parse("missing PLY format token"))?;
120                    let version =
121                        parts.next().ok_or_else(|| ply_parse("missing PLY format version"))?;
122                    if version != "1.0" {
123                        return Err(ply_format(format!("unsupported PLY version `{version}`")));
124                    }
125                    format = Some(match token {
126                        "ascii" => PlyFormat::Ascii,
127                        "binary_little_endian" => PlyFormat::BinaryLittleEndian,
128                        "binary_big_endian" => {
129                            return Err(ply_format("binary_big_endian PLY is not supported"));
130                        }
131                        _ => return Err(ply_format(format!("unsupported PLY format `{token}`"))),
132                    });
133                }
134                "element" => {
135                    let name = parts
136                        .next()
137                        .ok_or_else(|| ply_parse("missing PLY element name"))?
138                        .to_owned();
139                    let count = parts
140                        .next()
141                        .ok_or_else(|| ply_parse("missing PLY element count"))?
142                        .parse::<usize>()
143                        .map_err(|_| ply_parse("invalid PLY element count"))?;
144                    if name == "vertex" {
145                        if vertex_count.is_some() {
146                            return Err(ply_format("duplicate vertex element in PLY header"));
147                        }
148                        vertex_count = Some(count);
149                    } else {
150                        return Err(ply_format(format!(
151                            "unsupported PLY element `{name}` (only vertex is supported)"
152                        )));
153                    }
154                    current_element = Some(name);
155                }
156                "property" => {
157                    let element = current_element
158                        .as_deref()
159                        .ok_or_else(|| ply_parse("PLY property declared before element"))?;
160                    if element != "vertex" {
161                        return Err(ply_format("only vertex properties are supported"));
162                    }
163                    let kind_token =
164                        parts.next().ok_or_else(|| ply_parse("missing PLY property type"))?;
165                    let name = parts
166                        .next()
167                        .ok_or_else(|| ply_parse("missing PLY property name"))?
168                        .to_owned();
169                    properties
170                        .push(PlyProperty { name, kind: PlyPropertyKind::parse(kind_token)? });
171                }
172                "comment" | "obj_info" => {}
173                _ => return Err(ply_parse(format!("unsupported PLY header keyword `{keyword}`"))),
174            }
175        }
176
177        Ok(Self {
178            format: format.ok_or_else(|| ply_format("missing PLY format line"))?,
179            vertex_count: vertex_count.ok_or_else(|| ply_format("missing vertex element"))?,
180            properties,
181        })
182    }
183
184    /// Returns the byte size of one binary vertex record.
185    #[must_use]
186    pub fn vertex_stride(&self) -> usize {
187        self.properties.iter().map(|property| property.kind.size_bytes()).sum()
188    }
189
190    /// Writes a vertex-only PLY header.
191    pub fn write_header<W: std::io::Write>(&self, writer: &mut W) -> Result<(), IoError> {
192        writeln!(writer, "ply")?;
193        let format = match self.format {
194            PlyFormat::Ascii => "ascii 1.0",
195            PlyFormat::BinaryLittleEndian => "binary_little_endian 1.0",
196        };
197        writeln!(writer, "format {format}")?;
198        writeln!(writer, "element vertex {}", self.vertex_count)?;
199        for property in &self.properties {
200            writeln!(writer, "property {} {}", property.kind.as_token(), property.name)?;
201        }
202        writeln!(writer, "end_header")?;
203        Ok(())
204    }
205}
206
207fn read_non_empty_line<R: BufRead>(reader: &mut R) -> Result<String, IoError> {
208    loop {
209        let mut line = String::new();
210        let read = reader.read_line(&mut line)?;
211        if read == 0 {
212            return Err(ply_parse("unexpected EOF while reading PLY header"));
213        }
214        let trimmed = line.trim();
215        if trimmed.is_empty() {
216            continue;
217        }
218        return Ok(trimmed.to_owned());
219    }
220}