Skip to main content

spatialrust_io/las/
writer.rs

1use std::io::{Seek, Write};
2
3use las::point::{Classification, Format};
4use las::{Builder, Color, Header, Point, Writer};
5use spatialrust_core::{FieldSemantic, HasPositions3, PointCloud, PointField, PointSchema};
6
7use crate::error::{las_format, las_parse, IoError};
8use crate::las::schema::schema_from_point_cloud;
9use crate::{PointWriter, WriteOptions};
10
11/// Output encoding for LAS writers.
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
13pub enum LasWriteFormat {
14    /// Uncompressed LAS.
15    #[default]
16    Las,
17    /// LAZ compression (requires `io-laz` feature).
18    Laz,
19}
20
21/// Writes point clouds to LAS/LAZ files or streams.
22pub struct LasWriter<W: Write + Seek + Send + Sync + 'static> {
23    writer: W,
24    format: LasWriteFormat,
25}
26
27impl<W: Write + Seek + Send + Sync + 'static> LasWriter<W> {
28    /// Creates a new LAS writer.
29    #[must_use]
30    pub const fn new(writer: W, format: LasWriteFormat) -> Self {
31        Self { writer, format }
32    }
33}
34
35impl<W: Write + Seek + Send + Sync + 'static> PointWriter for LasWriter<W> {
36    fn write(
37        &mut self,
38        cloud: &PointCloud,
39        _options: &WriteOptions,
40    ) -> spatialrust_core::SpatialResult<()> {
41        let buffer = write_las(std::io::Cursor::new(Vec::new()), cloud, self.format)
42            .map_err(|error| spatialrust_core::SpatialError::Io(error.to_string()))?;
43        self.writer
44            .write_all(buffer.get_ref())
45            .map_err(|error| spatialrust_core::SpatialError::Io(error.to_string()))
46    }
47}
48
49/// Writes a point cloud to a LAS/LAZ stream and returns the inner writer.
50pub fn write_las<W: Write + Seek + Send + Sync + 'static>(
51    writer: W,
52    cloud: &PointCloud,
53    format: LasWriteFormat,
54) -> Result<W, IoError> {
55    cloud.validate()?;
56    if format == LasWriteFormat::Laz {
57        #[cfg(not(feature = "io-laz"))]
58        {
59            return Err(crate::error::laz_format(
60                "LAZ output requires the io-laz feature".to_owned(),
61            ));
62        }
63    }
64
65    let (point_format, export_schema) = schema_from_point_cloud(cloud.schema())?;
66    let header = header_from_cloud(point_format, format)?;
67    let mut las_writer =
68        Writer::new(writer, header).map_err(|error| las_format(error.to_string()))?;
69
70    for index in 0..cloud.len() {
71        let point = point_from_cloud(cloud, &export_schema, index, point_format)?;
72        las_writer.write_point(point).map_err(|error| las_format(error.to_string()))?;
73    }
74
75    las_writer.into_inner().map_err(|error| las_format(error.to_string()))
76}
77
78/// Writes a point cloud to a LAS/LAZ file on disk.
79pub fn write_las_file(
80    path: impl AsRef<std::path::Path>,
81    cloud: &PointCloud,
82    format: LasWriteFormat,
83) -> Result<(), IoError> {
84    if format == LasWriteFormat::Laz {
85        #[cfg(not(feature = "io-laz"))]
86        {
87            return Err(crate::error::laz_format(
88                "LAZ output requires the io-laz feature".to_owned(),
89            ));
90        }
91    }
92
93    let (point_format, export_schema) = schema_from_point_cloud(cloud.schema())?;
94    let header = header_from_cloud(point_format, format)?;
95    let mut las_writer =
96        Writer::from_path(path.as_ref(), header).map_err(|error| las_format(error.to_string()))?;
97
98    for index in 0..cloud.len() {
99        let point = point_from_cloud(cloud, &export_schema, index, point_format)?;
100        las_writer.write_point(point).map_err(|error| las_format(error.to_string()))?;
101    }
102
103    las_writer.close().map_err(|error| las_format(error.to_string()))
104}
105
106fn header_from_cloud(point_format: Format, format: LasWriteFormat) -> Result<Header, IoError> {
107    let mut builder = Builder::from((1, 2));
108    builder.point_format = point_format;
109    builder.system_identifier = "SpatialRust".to_owned();
110    builder.generating_software = "SpatialRust".to_owned();
111
112    if format == LasWriteFormat::Laz {
113        builder.point_format.is_compressed = true;
114    }
115
116    builder.into_header().map_err(|error| las_format(error.to_string()))
117}
118
119pub(crate) fn point_from_cloud(
120    cloud: &PointCloud,
121    schema: &PointSchema,
122    index: usize,
123    format: Format,
124) -> Result<Point, IoError> {
125    let (x, y, z) = cloud.positions3()?;
126    let mut point = Point {
127        x: f64::from(x[index]),
128        y: f64::from(y[index]),
129        z: f64::from(z[index]),
130        ..Default::default()
131    };
132
133    for field in schema.fields() {
134        if matches!(
135            field.semantic,
136            FieldSemantic::PositionX | FieldSemantic::PositionY | FieldSemantic::PositionZ
137        ) {
138            continue;
139        }
140        let Some(field_name) = cloud_field_name_for_export(cloud, field) else {
141            continue;
142        };
143        let value = read_cloud_field(cloud, field_name, index)?;
144        apply_las_field(&mut point, field, value, format)?;
145    }
146
147    Ok(point)
148}
149
150fn cloud_field_name_for_export<'a>(
151    cloud: &'a PointCloud,
152    export_field: &'a PointField,
153) -> Option<&'a str> {
154    if cloud.field(&export_field.name).is_ok() {
155        return Some(export_field.name.as_str());
156    }
157    cloud
158        .schema()
159        .find_semantic(export_field.semantic)
160        .map(|field| field.name.as_str())
161        .filter(|name| cloud.field(name).is_ok())
162}
163
164fn read_cloud_field(cloud: &PointCloud, field_name: &str, index: usize) -> Result<f64, IoError> {
165    let buffer = cloud.field(field_name)?;
166    Ok(match buffer {
167        PointBuffer::F32(values) => f64::from(values[index]),
168        PointBuffer::F64(values) => values[index],
169        PointBuffer::U8(values) => f64::from(values[index]),
170        PointBuffer::U16(values) => f64::from(values[index]),
171        PointBuffer::I32(values) => f64::from(values[index]),
172        PointBuffer::U32(values) => f64::from(values[index]),
173    })
174}
175
176use spatialrust_core::PointBuffer;
177
178fn apply_las_field(
179    point: &mut Point,
180    field: &PointField,
181    value: f64,
182    format: Format,
183) -> Result<(), IoError> {
184    match field.semantic {
185        FieldSemantic::Intensity => point.intensity = value.round() as u16,
186        FieldSemantic::Label => {
187            point.classification = Classification::new(value.round() as u8)
188                .map_err(|error| las_parse(error.to_string()))?;
189        }
190        FieldSemantic::TimeOffset => {
191            if format.has_gps_time {
192                point.gps_time = Some(value);
193            }
194        }
195        FieldSemantic::ColorR | FieldSemantic::ColorG | FieldSemantic::ColorB => {
196            if format.has_color {
197                let color = point.color.get_or_insert_with(Color::default);
198                match field.semantic {
199                    FieldSemantic::ColorR => color.red = value.round() as u16,
200                    FieldSemantic::ColorG => color.green = value.round() as u16,
201                    FieldSemantic::ColorB => color.blue = value.round() as u16,
202                    _ => {}
203                }
204            }
205        }
206        FieldSemantic::PositionX | FieldSemantic::PositionY | FieldSemantic::PositionZ => {}
207        _ => {}
208    }
209    Ok(())
210}
211
212#[cfg(test)]
213mod tests {
214    use super::{write_las, LasWriteFormat};
215    use spatialrust_core::PointCloudBuilder;
216    use std::io::Cursor;
217
218    #[test]
219    fn writes_xyz_cloud() {
220        let mut builder = PointCloudBuilder::xyz();
221        builder.push_point([1.0, 2.0, 3.0]).unwrap();
222        let cloud = builder.build().unwrap();
223        let bytes = write_las(Cursor::new(Vec::new()), &cloud, LasWriteFormat::Las).unwrap();
224        assert!(!bytes.get_ref().is_empty());
225    }
226
227    #[test]
228    fn writes_xyzirgb_cloud_with_u8_color_fields() {
229        use spatialrust_core::StandardSchemas;
230
231        let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyzirgb());
232        builder.push_point([1.0, 2.0, 3.0, 100.0, 255.0, 128.0, 64.0]).unwrap();
233        let cloud = builder.build().unwrap();
234        let bytes = write_las(Cursor::new(Vec::new()), &cloud, LasWriteFormat::Las).unwrap();
235        assert!(!bytes.get_ref().is_empty());
236    }
237
238    #[cfg(feature = "io-laz")]
239    #[test]
240    fn writes_laz_file() {
241        use super::write_las_file;
242
243        let mut builder = PointCloudBuilder::xyz();
244        builder.push_point([0.0, 0.0, 0.0]).unwrap();
245        let cloud = builder.build().unwrap();
246        let path =
247            std::env::temp_dir().join(format!("spatialrust_laz_write_{}.laz", std::process::id()));
248        write_las_file(&path, &cloud, LasWriteFormat::Laz).unwrap();
249        let loaded = crate::las::read_las_file(&path).unwrap();
250        let _ = std::fs::remove_file(path);
251        assert_eq!(loaded.len(), 1);
252    }
253}