1use 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#[derive(Debug, Clone)]
25pub struct PlyImport {
26 pub vertices: PointCloud,
29 pub triangles: Vec<[u32; 3]>,
32 pub triangle_colours: Option<Vec<[f64; 4]>>,
35 pub warnings: Vec<String>,
39}
40
41impl PlyImport {
42 #[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#[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 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 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
177fn 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 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 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
277struct 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 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 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
399fn 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
407fn 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
421pub 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
599fn 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
643const RESERVED: [&str; 10] = [
645 "x", "y", "z", "nx", "ny", "nz", "red", "green", "blue", "alpha",
646];
647
648pub 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 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 #[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 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 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}