Skip to main content

point_formats/io/
mod.rs

1//! Native readers and writers.
2
3pub mod delimited;
4pub mod obj;
5pub mod pcd;
6pub mod ply;
7pub mod pts;
8pub mod ptx;
9pub mod stl;
10
11#[cfg(feature = "copc")]
12pub mod copc;
13#[cfg(feature = "e57")]
14pub mod e57;
15#[cfg(feature = "geospatial")]
16pub mod geojson;
17#[cfg(feature = "geospatial")]
18pub mod geotiff;
19#[cfg(feature = "las")]
20pub mod las;
21
22pub mod asciigrid;
23#[cfg(feature = "dxf")]
24pub mod dxf;
25#[cfg(feature = "gltf")]
26pub mod gltf;
27#[cfg(feature = "gpkg")]
28pub mod gpkg;
29#[cfg(feature = "robotics")]
30pub mod robotics;
31#[cfg(feature = "sensor")]
32pub mod sensor;
33#[cfg(feature = "shapefile")]
34pub mod shapefile;
35
36use crate::error::{Error, Result};
37use crate::format::Format;
38use crate::types::Geometry;
39use std::fs::File;
40use std::io::{BufReader, BufWriter};
41use std::path::Path;
42
43/// Delimiter used by delimited text point files.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum Delimiter {
46    Auto,
47    Whitespace,
48    Comma,
49    Tab,
50    Semicolon,
51}
52
53impl Delimiter {
54    #[inline]
55    pub(crate) fn split_into_slice<'a>(self, line: &'a str, fields: &mut [&'a str]) -> usize {
56        let mut count = 0;
57        let limit = fields.len();
58        match self {
59            Self::Auto => Self::detect(line).split_into_slice(line, fields),
60            Self::Whitespace => {
61                for part in line.split_whitespace() {
62                    if count < limit {
63                        fields[count] = part;
64                        count += 1;
65                    } else {
66                        break;
67                    }
68                }
69                count
70            }
71            Self::Comma => {
72                for part in line.split(',') {
73                    if count < limit {
74                        fields[count] = part.trim();
75                        count += 1;
76                    } else {
77                        break;
78                    }
79                }
80                count
81            }
82            Self::Tab => {
83                for part in line.split('\t') {
84                    if count < limit {
85                        fields[count] = part.trim();
86                        count += 1;
87                    } else {
88                        break;
89                    }
90                }
91                count
92            }
93            Self::Semicolon => {
94                for part in line.split(';') {
95                    if count < limit {
96                        fields[count] = part.trim();
97                        count += 1;
98                    } else {
99                        break;
100                    }
101                }
102                count
103            }
104        }
105    }
106
107    #[inline]
108    pub(crate) fn detect(line: &str) -> Self {
109        if line.contains(',') {
110            Self::Comma
111        } else if line.contains(';') {
112            Self::Semicolon
113        } else if line.contains('\t') {
114            Self::Tab
115        } else {
116            Self::Whitespace
117        }
118    }
119
120    #[inline]
121    pub(crate) const fn as_str(self) -> &'static str {
122        match self {
123            Self::Auto | Self::Whitespace => " ",
124            Self::Comma => ",",
125            Self::Tab => "\t",
126            Self::Semicolon => ";",
127        }
128    }
129}
130
131/// Column positions for delimited text files. Missing optional columns are `None`.
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct ColumnMapping {
134    pub x: usize,
135    pub y: usize,
136    pub z: usize,
137    pub intensity: Option<usize>,
138    pub red: Option<usize>,
139    pub green: Option<usize>,
140    pub blue: Option<usize>,
141    pub classification: Option<usize>,
142    pub gps_time: Option<usize>,
143    pub normal_x: Option<usize>,
144    pub normal_y: Option<usize>,
145    pub normal_z: Option<usize>,
146}
147
148impl Default for ColumnMapping {
149    fn default() -> Self {
150        Self {
151            x: 0,
152            y: 1,
153            z: 2,
154            intensity: Some(3),
155            red: Some(4),
156            green: Some(5),
157            blue: Some(6),
158            classification: Some(7),
159            gps_time: Some(8),
160            normal_x: Some(9),
161            normal_y: Some(10),
162            normal_z: Some(11),
163        }
164    }
165}
166
167impl ColumnMapping {
168    pub(crate) fn from_header(header: &[&str]) -> Option<Self> {
169        fn find(header: &[&str], names: &[&str]) -> Option<usize> {
170            header.iter().position(|value| {
171                let normalized = value
172                    .trim()
173                    .trim_matches('"')
174                    .trim_matches('\'')
175                    .to_ascii_lowercase()
176                    .replace([' ', '-', '.'], "_");
177                names.iter().any(|candidate| normalized == *candidate)
178            })
179        }
180
181        let x = find(header, &["x", "easting", "east", "lon", "longitude"])?;
182        let y = find(header, &["y", "northing", "north", "lat", "latitude"])?;
183        let z = find(header, &["z", "elevation", "height", "altitude"])?;
184        Some(Self {
185            x,
186            y,
187            z,
188            intensity: find(header, &["intensity", "i"]),
189            red: find(header, &["red", "r"]),
190            green: find(header, &["green", "g"]),
191            blue: find(header, &["blue", "b"]),
192            classification: find(header, &["classification", "class", "label"]),
193            gps_time: find(header, &["gps_time", "gpstime", "time", "timestamp"]),
194            normal_x: find(header, &["normal_x", "nx", "n_x"]),
195            normal_y: find(header, &["normal_y", "ny", "n_y"]),
196            normal_z: find(header, &["normal_z", "nz", "n_z"]),
197        })
198    }
199}
200
201/// Options for XYZ/TXT/CSV-style formats.
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct DelimitedOptions {
204    pub delimiter: Delimiter,
205    /// `None` means autodetect by trying to parse the first non-comment line.
206    pub has_header: Option<bool>,
207    pub columns: ColumnMapping,
208    pub write_header: bool,
209    pub precision: usize,
210}
211
212impl Default for DelimitedOptions {
213    fn default() -> Self {
214        Self {
215            delimiter: Delimiter::Auto,
216            has_header: None,
217            columns: ColumnMapping::default(),
218            write_header: false,
219            precision: 6,
220        }
221    }
222}
223
224/// PLY encoding.
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub enum PlyEncoding {
227    Ascii,
228    BinaryLittleEndian,
229}
230
231/// PLY writer options.
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct PlyOptions {
234    pub encoding: PlyEncoding,
235    pub precision: usize,
236}
237
238impl Default for PlyOptions {
239    fn default() -> Self {
240        Self {
241            encoding: PlyEncoding::Ascii,
242            precision: 6,
243        }
244    }
245}
246
247/// PCD encoding.
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
249pub enum PcdEncoding {
250    Ascii,
251    Binary,
252}
253
254/// PCD writer options.
255#[derive(Debug, Clone, PartialEq, Eq)]
256pub struct PcdOptions {
257    pub encoding: PcdEncoding,
258    pub precision: usize,
259}
260
261impl Default for PcdOptions {
262    fn default() -> Self {
263        Self {
264            encoding: PcdEncoding::Ascii,
265            precision: 6,
266        }
267    }
268}
269
270/// STL writer options.
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct StlOptions {
273    pub binary: bool,
274}
275
276impl Default for StlOptions {
277    fn default() -> Self {
278        Self { binary: true }
279    }
280}
281
282/// Native reader/writer options.
283#[derive(Debug, Clone, PartialEq, Eq, Default)]
284pub struct NativeOptions {
285    pub delimited: DelimitedOptions,
286    pub ply: PlyOptions,
287    pub pcd: PcdOptions,
288    pub stl: StlOptions,
289}
290
291pub fn read_path(
292    path: impl AsRef<Path>,
293    format: Format,
294    options: &NativeOptions,
295) -> Result<Geometry> {
296    let path = path.as_ref();
297
298    #[cfg(feature = "shapefile")]
299    if matches!(format, Format::Shapefile) {
300        return shapefile::read(path);
301    }
302
303    #[cfg(feature = "gltf")]
304    if matches!(format, Format::Gltf | Format::Glb) {
305        return gltf::read(path);
306    }
307
308    #[cfg(feature = "gpkg")]
309    if matches!(format, Format::Gpkg) {
310        return gpkg::read(path);
311    }
312
313    #[cfg(feature = "robotics")]
314    if matches!(format, Format::RosBag) {
315        return robotics::read_rosbag(path);
316    }
317
318    #[cfg(feature = "robotics")]
319    if matches!(format, Format::Ros2Bag) {
320        return robotics::read_ros2bag(path);
321    }
322
323    #[cfg(feature = "robotics")]
324    if matches!(format, Format::PointCloud2) {
325        return robotics::read_pc2(path);
326    }
327
328    #[cfg(feature = "sensor")]
329    if matches!(format, Format::Pcap) {
330        return sensor::read_pcap(path);
331    }
332
333    #[cfg(feature = "sensor")]
334    if matches!(format, Format::UdpPackets) {
335        return sensor::read_udppackets(path);
336    }
337
338    #[cfg(feature = "sensor")]
339    if matches!(format, Format::VendorRaw) {
340        return sensor::read_vendorraw(path);
341    }
342
343    let file = File::open(path)?;
344    let mut reader = BufReader::new(file);
345    match format {
346        Format::Xyz | Format::Txt | Format::Csv => {
347            let mut opts = options.delimited.clone();
348            if matches!(format, Format::Csv) && matches!(opts.delimiter, Delimiter::Auto) {
349                opts.delimiter = Delimiter::Comma;
350                opts.write_header = true;
351            }
352            delimited::read(&mut reader, format, &opts).map(Geometry::PointCloud)
353        }
354        Format::Pts => pts::read(&mut reader).map(Geometry::PointCloud),
355        Format::Ptx => ptx::read(&mut reader).map(Geometry::PointCloud),
356        Format::Ply => ply::read(&mut reader),
357        Format::Pcd => pcd::read(&mut reader),
358        Format::Obj => obj::read(&mut reader),
359        Format::Stl => stl::read(&mut reader),
360        Format::AsciiGrid => asciigrid::read(&mut reader),
361
362        #[cfg(feature = "dxf")]
363        Format::Dxf => dxf::read(&mut reader),
364
365        #[cfg(feature = "las")]
366        Format::Las | Format::Laz => las::read(reader),
367
368        #[cfg(feature = "copc")]
369        Format::Copc => copc::read(&mut reader),
370
371        #[cfg(feature = "e57")]
372        Format::E57 => e57::read(&mut reader),
373
374        #[cfg(feature = "geospatial")]
375        Format::GeoTiff | Format::Cog => geotiff::read(reader),
376
377        #[cfg(feature = "geospatial")]
378        Format::GeoJson => geojson::read(reader),
379
380        _ => Err(Error::unsupported(format, "read", format.adapter_hint())),
381    }
382}
383
384pub fn write_path(
385    path: impl AsRef<Path>,
386    format: Format,
387    geometry: &Geometry,
388    options: &NativeOptions,
389) -> Result<()> {
390    let path = path.as_ref();
391
392    #[cfg(feature = "e57")]
393    if matches!(format, Format::E57) {
394        let cloud = as_cloud_for_point_format(geometry, format)?;
395        return e57::write_to_path(path, cloud);
396    }
397
398    #[cfg(feature = "shapefile")]
399    if matches!(format, Format::Shapefile) {
400        let cloud = as_cloud_for_point_format(geometry, format)?;
401        return shapefile::write(path, cloud);
402    }
403
404    #[cfg(feature = "gltf")]
405    if matches!(format, Format::Gltf) {
406        return gltf::write_gltf(path, geometry);
407    }
408
409    #[cfg(feature = "gltf")]
410    if matches!(format, Format::Glb) {
411        return gltf::write_glb(path, geometry);
412    }
413
414    #[cfg(feature = "gpkg")]
415    if matches!(format, Format::Gpkg) {
416        let cloud = as_cloud_for_point_format(geometry, format)?;
417        return gpkg::write(path, cloud);
418    }
419
420    #[cfg(feature = "robotics")]
421    if matches!(format, Format::RosBag) {
422        let cloud = as_cloud_for_point_format(geometry, format)?;
423        return robotics::write_rosbag(path, cloud);
424    }
425
426    #[cfg(feature = "robotics")]
427    if matches!(format, Format::Ros2Bag) {
428        let cloud = as_cloud_for_point_format(geometry, format)?;
429        return robotics::write_ros2bag(path, cloud);
430    }
431
432    #[cfg(feature = "robotics")]
433    if matches!(format, Format::PointCloud2) {
434        let cloud = as_cloud_for_point_format(geometry, format)?;
435        return robotics::write_pc2(path, cloud);
436    }
437
438    #[cfg(feature = "sensor")]
439    if matches!(format, Format::Pcap) {
440        let cloud = as_cloud_for_point_format(geometry, format)?;
441        return sensor::write_pcap(path, cloud);
442    }
443
444    #[cfg(feature = "sensor")]
445    if matches!(format, Format::UdpPackets) {
446        let cloud = as_cloud_for_point_format(geometry, format)?;
447        return sensor::write_udppackets(path, cloud);
448    }
449
450    #[cfg(feature = "sensor")]
451    if matches!(format, Format::VendorRaw) {
452        let cloud = as_cloud_for_point_format(geometry, format)?;
453        return sensor::write_vendorraw(path, cloud);
454    }
455
456    let file = File::create(path)?;
457    let mut writer = BufWriter::new(file);
458    match format {
459        Format::Xyz | Format::Txt | Format::Csv => {
460            let cloud = as_cloud_for_point_format(geometry, format)?;
461            let mut opts = options.delimited.clone();
462            if matches!(format, Format::Csv) {
463                if matches!(opts.delimiter, Delimiter::Auto) {
464                    opts.delimiter = Delimiter::Comma;
465                }
466                opts.write_header = true;
467            }
468            delimited::write(&mut writer, format, cloud, &opts)
469        }
470        Format::Pts => pts::write(&mut writer, as_cloud_for_point_format(geometry, format)?),
471        Format::Ptx => ptx::write(&mut writer, as_cloud_for_point_format(geometry, format)?),
472        Format::Ply => ply::write(&mut writer, geometry, &options.ply),
473        Format::Pcd => pcd::write(
474            &mut writer,
475            as_cloud_for_point_format(geometry, format)?,
476            &options.pcd,
477        ),
478        Format::Obj => obj::write(&mut writer, geometry),
479        Format::Stl => stl::write(&mut writer, geometry, &options.stl),
480        Format::AsciiGrid => {
481            let cloud = as_cloud_for_point_format(geometry, format)?;
482            asciigrid::write(&mut writer, cloud)
483        }
484
485        #[cfg(feature = "dxf")]
486        Format::Dxf => dxf::write(&mut writer, geometry),
487
488        #[cfg(feature = "las")]
489        Format::Las | Format::Laz => {
490            let cloud = as_cloud_for_point_format(geometry, format)?;
491            las::write(writer, cloud)
492        }
493
494        #[cfg(feature = "geospatial")]
495        Format::GeoTiff | Format::Cog => {
496            let cloud = as_cloud_for_point_format(geometry, format)?;
497            geotiff::write(writer, cloud)
498        }
499
500        #[cfg(feature = "geospatial")]
501        Format::GeoJson => {
502            let cloud = as_cloud_for_point_format(geometry, format)?;
503            geojson::write(writer, cloud)
504        }
505
506        _ => Err(Error::unsupported(format, "write", format.adapter_hint())),
507    }
508}
509
510fn as_cloud_for_point_format(
511    geometry: &Geometry,
512    format: Format,
513) -> Result<&crate::types::PointCloud> {
514    match geometry {
515        Geometry::PointCloud(cloud) => Ok(cloud),
516        Geometry::Mesh(_) => Err(Error::LossyConversionBlocked {
517            from: "mesh",
518            to: format,
519            reason: "the destination is a point-cloud format and cannot preserve faces".to_string(),
520        }),
521    }
522}
523
524#[cold]
525#[inline(never)]
526fn numeric_parse_error(format: Format, line: usize, name: &str, value: &str) -> Error {
527    Error::parse(
528        format,
529        line,
530        format!("expected numeric {name}, got '{value}'"),
531    )
532}
533
534#[inline]
535pub(crate) fn parse_f64(format: Format, line: usize, name: &str, value: &str) -> Result<f64> {
536    value
537        .parse::<f64>()
538        .map_err(|_| numeric_parse_error(format, line, name, value))
539}
540
541#[inline]
542pub(crate) fn parse_f32(format: Format, line: usize, name: &str, value: &str) -> Result<f32> {
543    value
544        .parse::<f32>()
545        .map_err(|_| numeric_parse_error(format, line, name, value))
546}
547
548#[cold]
549#[inline(never)]
550fn range_parse_error(format: Format, line: usize, name: &str, value: &str, limit: &str) -> Error {
551    Error::parse(
552        format,
553        line,
554        format!("expected {name} in range {limit}, got '{value}'"),
555    )
556}
557
558#[inline]
559pub(crate) fn parse_u8(format: Format, line: usize, name: &str, value: &str) -> Result<u8> {
560    if let Ok(v) = value.parse::<u8>() {
561        return Ok(v);
562    }
563    let as_float = parse_f64(format, line, name, value)?;
564    if as_float.fract() == 0.0 && (0.0..=u8::MAX as f64).contains(&as_float) {
565        Ok(as_float as u8)
566    } else {
567        Err(range_parse_error(format, line, name, value, "0..255"))
568    }
569}
570
571#[inline]
572pub(crate) fn parse_u16(format: Format, line: usize, name: &str, value: &str) -> Result<u16> {
573    if let Ok(v) = value.parse::<u16>() {
574        return Ok(v);
575    }
576    let as_float = parse_f64(format, line, name, value)?;
577    if as_float.fract() == 0.0 && (0.0..=u16::MAX as f64).contains(&as_float) {
578        Ok(as_float as u16)
579    } else {
580        Err(range_parse_error(format, line, name, value, "0..65535"))
581    }
582}
583
584#[inline]
585pub(crate) fn write_fmt_f64<W: std::io::Write>(
586    writer: &mut W,
587    value: f64,
588    precision: usize,
589) -> std::io::Result<()> {
590    if value == 0.0 {
591        write!(writer, "{:.*}", precision, 0.0)
592    } else {
593        write!(writer, "{:.*}", precision, value)
594    }
595}
596
597#[inline]
598pub(crate) fn fmt_f64(value: f64, precision: usize) -> String {
599    if value == 0.0 {
600        // Avoid writing -0.000000 after transforms/triangulation.
601        return format!("{:.*}", precision, 0.0);
602    }
603    format!("{:.*}", precision, value)
604}
605
606#[inline]
607pub(crate) fn write_f32_le<W: std::io::Write>(writer: &mut W, value: f32) -> Result<()> {
608    writer.write_all(&value.to_le_bytes())?;
609    Ok(())
610}
611
612#[inline]
613pub(crate) fn write_f64_le<W: std::io::Write>(writer: &mut W, value: f64) -> Result<()> {
614    writer.write_all(&value.to_le_bytes())?;
615    Ok(())
616}
617
618#[inline]
619pub(crate) fn write_u16_le<W: std::io::Write>(writer: &mut W, value: u16) -> Result<()> {
620    writer.write_all(&value.to_le_bytes())?;
621    Ok(())
622}
623
624#[inline]
625pub(crate) fn write_u32_le<W: std::io::Write>(writer: &mut W, value: u32) -> Result<()> {
626    writer.write_all(&value.to_le_bytes())?;
627    Ok(())
628}
629
630#[inline]
631pub(crate) fn read_exact<const N: usize, R: std::io::Read>(reader: &mut R) -> Result<[u8; N]> {
632    let mut bytes = [0_u8; N];
633    reader.read_exact(&mut bytes)?;
634    Ok(bytes)
635}
636
637#[inline]
638pub(crate) fn read_f32_le<R: std::io::Read>(reader: &mut R) -> Result<f32> {
639    Ok(f32::from_le_bytes(read_exact(reader)?))
640}
641
642#[inline]
643pub(crate) fn read_f64_le<R: std::io::Read>(reader: &mut R) -> Result<f64> {
644    Ok(f64::from_le_bytes(read_exact(reader)?))
645}
646
647#[inline]
648pub(crate) fn read_u16_le<R: std::io::Read>(reader: &mut R) -> Result<u16> {
649    Ok(u16::from_le_bytes(read_exact(reader)?))
650}
651
652#[inline]
653pub(crate) fn read_u32_le<R: std::io::Read>(reader: &mut R) -> Result<u32> {
654    Ok(u32::from_le_bytes(read_exact(reader)?))
655}
656
657#[cfg(test)]
658mod tests {
659    use super::*;
660    use std::io::Cursor;
661
662    #[test]
663    fn test_delimiter_logic() {
664        assert_eq!(Delimiter::detect("1,2,3"), Delimiter::Comma);
665        assert_eq!(Delimiter::detect("1;2;3"), Delimiter::Semicolon);
666        assert_eq!(Delimiter::detect("1\t2\t3"), Delimiter::Tab);
667        assert_eq!(Delimiter::detect("1 2 3"), Delimiter::Whitespace);
668
669        assert_eq!(Delimiter::Comma.as_str(), ",");
670        assert_eq!(Delimiter::Tab.as_str(), "\t");
671        assert_eq!(Delimiter::Semicolon.as_str(), ";");
672        assert_eq!(Delimiter::Auto.as_str(), " ");
673
674        let mut fields = [""; 4];
675        let count = Delimiter::Comma.split_into_slice("a, b , c", &mut fields);
676        assert_eq!(count, 3);
677        assert_eq!(fields[0], "a");
678        assert_eq!(fields[1], "b");
679        assert_eq!(fields[2], "c");
680
681        let count_tab = Delimiter::Tab.split_into_slice("a\tb\tc", &mut fields);
682        assert_eq!(count_tab, 3);
683        assert_eq!(fields[1], "b");
684
685        let count_semi = Delimiter::Semicolon.split_into_slice("a;b;c", &mut fields);
686        assert_eq!(count_semi, 3);
687        assert_eq!(fields[1], "b");
688
689        let count_white = Delimiter::Whitespace.split_into_slice("a b  c", &mut fields);
690        assert_eq!(count_white, 3);
691        assert_eq!(fields[2], "c");
692
693        let count_auto = Delimiter::Auto.split_into_slice("a,b,c", &mut fields);
694        assert_eq!(count_auto, 3);
695        assert_eq!(fields[1], "b");
696    }
697
698    #[test]
699    fn test_column_mapping() {
700        let header = [
701            "x",
702            "y",
703            "elevation",
704            "intensity",
705            "red",
706            "green",
707            "blue",
708            "classification",
709            "gps_time",
710            "normal_x",
711            "normal_y",
712            "normal_z",
713        ];
714        let mapping = ColumnMapping::from_header(&header).unwrap();
715        assert_eq!(mapping.x, 0);
716        assert_eq!(mapping.y, 1);
717        assert_eq!(mapping.z, 2);
718        assert_eq!(mapping.intensity, Some(3));
719        assert_eq!(mapping.red, Some(4));
720        assert_eq!(mapping.green, Some(5));
721        assert_eq!(mapping.blue, Some(6));
722        assert_eq!(mapping.classification, Some(7));
723        assert_eq!(mapping.gps_time, Some(8));
724        assert_eq!(mapping.normal_x, Some(9));
725
726        let bad_header = ["intensity"];
727        assert!(ColumnMapping::from_header(&bad_header).is_none());
728    }
729
730    #[test]
731    fn test_numeric_writing_and_parsing() {
732        let mut writer = Vec::new();
733        write_fmt_f64(&mut writer, 0.0, 4).unwrap();
734        assert_eq!(String::from_utf8(writer).unwrap(), "0.0000");
735
736        let mut writer2 = Vec::new();
737        write_fmt_f64(&mut writer2, 1.234, 2).unwrap();
738        assert_eq!(String::from_utf8(writer2).unwrap(), "1.23");
739
740        assert_eq!(fmt_f64(0.0, 3), "0.000");
741        assert_eq!(fmt_f64(-1.23, 1), "-1.2");
742
743        assert!(parse_f64(Format::Pts, 1, "test", "abc").is_err());
744        assert!(parse_f32(Format::Pts, 1, "test", "abc").is_err());
745
746        assert_eq!(parse_u8(Format::Pts, 1, "test", "123").unwrap(), 123);
747        assert_eq!(parse_u8(Format::Pts, 1, "test", "123.0").unwrap(), 123);
748        assert!(parse_u8(Format::Pts, 1, "test", "256").is_err());
749        assert!(parse_u8(Format::Pts, 1, "test", "abc").is_err());
750        assert!(parse_u8(Format::Pts, 1, "test", "1.5").is_err());
751
752        assert_eq!(parse_u16(Format::Pts, 1, "test", "1000").unwrap(), 1000);
753        assert_eq!(parse_u16(Format::Pts, 1, "test", "1000.0").unwrap(), 1000);
754        assert!(parse_u16(Format::Pts, 1, "test", "70000").is_err());
755        assert!(parse_u16(Format::Pts, 1, "test", "abc").is_err());
756        assert!(parse_u16(Format::Pts, 1, "test", "1.5").is_err());
757    }
758
759    #[test]
760    fn test_binary_helpers() {
761        let mut bin = Vec::new();
762        write_f32_le(&mut bin, 1.5f32).unwrap();
763        write_f64_le(&mut bin, 2.5f64).unwrap();
764        write_u16_le(&mut bin, 10u16).unwrap();
765        write_u32_le(&mut bin, 20u32).unwrap();
766
767        let mut cursor = Cursor::new(bin);
768        assert_eq!(read_f32_le(&mut cursor).unwrap(), 1.5f32);
769        assert_eq!(read_f64_le(&mut cursor).unwrap(), 2.5f64);
770        assert_eq!(read_u16_le(&mut cursor).unwrap(), 10u16);
771        assert_eq!(read_u32_le(&mut cursor).unwrap(), 20u32);
772    }
773}