Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project will be documented in this file.

## [Unreleased]

- Added support for reading and writing `pointGroupingSchemes/groupingByLine`,
the optional per-scan-line index of a structured point cloud.
See `PointCloudWriter::set_point_groups` and `E57Reader::point_groups`.

## [0.11.13] - 2026-06-17

- Improved error messages (thx @chpatrick and @nh2)
Expand Down
39 changes: 39 additions & 0 deletions src/e57_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use crate::Image;
use crate::PointCloud;
use crate::PointCloudReaderRaw;
use crate::PointCloudReaderSimple;
use crate::PointGroup;
use crate::RecordValue;
use crate::Result;
use roxmltree::Document;
use std::fs::File;
Expand Down Expand Up @@ -117,6 +119,43 @@ impl<T: Read + Seek> E57Reader<T> {
PointCloudReaderRaw::new(pc, &mut self.reader)
}

/// Reads the `groupingByLine` scheme of a point cloud, if it has one:
/// which points belong to which row or column.
pub fn point_groups(&mut self, pc: &PointCloud) -> Result<Option<Vec<PointGroup>>> {
let Some(header) = pc.point_groups.clone() else {
return Ok(None);
};

// The groups are an ordinary compressed vector, so they can be read by
// the same machinery as points once they are described as one.
let as_pointcloud = PointCloud {
file_offset: header.file_offset,
records: header.records,
prototype: header.prototype(),
..Default::default()
};

let mut groups = Vec::with_capacity(header.records as usize);
for values in PointCloudReaderRaw::new(&as_pointcloud, &mut self.reader)? {
let values = values?;
if values.len() != 3 {
Error::invalid("A point group must have exactly three values")?
}
let integer = |v: &RecordValue| -> Result<i64> {
match v {
RecordValue::Integer(i) => Ok(*i),
_ => Error::invalid("A point group value must be an integer"),
}
};
groups.push(PointGroup {
start_point_index: integer(&values[0])?,
id_element_value: integer(&values[1])?,
point_count: integer(&values[2])?,
});
}
Ok(Some(groups))
}

/// Returns a list of all image descriptors in the file.
pub fn images(&self) -> Vec<Image> {
self.images.clone()
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ mod pc_reader_raw;
mod pc_reader_simple;
mod pc_writer;
mod point;
mod point_groups;
mod pointcloud;
mod queue_reader;
mod record;
Expand Down Expand Up @@ -89,6 +90,10 @@ pub use self::point::CartesianCoordinate;
pub use self::point::Color;
pub use self::point::Point;
pub use self::point::SphericalCoordinate;
pub use self::point_groups::PointGroup;
pub use self::point_groups::PointGroupLimits;
pub use self::point_groups::PointGroups;
pub use self::point_groups::PointGroupsHeader;
pub use self::pointcloud::PointCloud;
pub use self::record::Record;
pub use self::record::RecordDataType;
Expand Down
150 changes: 150 additions & 0 deletions src/pc_writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use crate::Error;
use crate::IndexBounds;
use crate::IntensityLimits;
use crate::PointCloud;
use crate::PointGroups;
use crate::PointGroupsHeader;
use crate::RawValues;
use crate::Record;
use crate::RecordDataType;
Expand Down Expand Up @@ -58,6 +60,7 @@ pub struct PointCloudWriter<'a, T: Read + Write + Seek> {
temperature: Option<f64>,
humidity: Option<f64>,
atmospheric_pressure: Option<f64>,
point_groups: Option<PointGroups>,
}

impl<'a, T: Read + Write + Seek> PointCloudWriter<'a, T> {
Expand Down Expand Up @@ -169,6 +172,7 @@ impl<'a, T: Read + Write + Seek> PointCloudWriter<'a, T> {
index_bounds,
color_limits,
intensity_limits,
point_groups: None,
name: None,
description: None,
transform: None,
Expand Down Expand Up @@ -316,6 +320,19 @@ impl<'a, T: Read + Write + Seek> PointCloudWriter<'a, T> {
self.cartesian_bounds = value;
}

/// Sets the `groupingByLine` scheme of the point cloud: the optional
/// per-scan-line index that lets a reader seek to a single row or column
/// without decoding the whole cloud.
///
/// The groups are written as their own compressed vector when the point
/// cloud is finalized, so this has to be set **after** the last point has
/// been added and before calling `finalize()`. The caller is responsible
/// for the groups matching the points: they are not derived, because the
/// declared limits sometimes have to match another implementation exactly.
pub fn set_point_groups(&mut self, value: Option<PointGroups>) {
self.point_groups = value;
}

/// Sets and overrides the spherical bounds of the point cloud manually.
/// This should not be used in normales use cases, since the bounds
/// are set and updated automatically when you add a point that contains
Expand Down Expand Up @@ -597,6 +614,131 @@ impl<'a, T: Read + Write + Seek> PointCloudWriter<'a, T> {
Ok(())
}

/// Lays down the groups as a compressed vector section of their own and
/// returns the header describing where it went.
fn write_point_groups(&mut self, groups: &PointGroups) -> Result<PointGroupsHeader> {
// Fail before writing anything if the scheme is not one this format
// can express.
groups.id_element_tag()?;

// Every value has to fit the limits it is declared under, or the file
// would encode it in too few bits and silently lose it. libE57Format
// rejects this case with E57_ERROR_VALUE_OUT_OF_BOUNDS, and so do we.
let limits = groups.limits;
for group in &groups.groups {
let check = |value: i64, max: i64, field: &str| -> Result<()> {
if value < 0 || value > max {
Error::invalid(format!(
"Point group {field} is {value}, outside its declared range of 0..{max}"
))?
}
Ok(())
};
check(
group.start_point_index,
limits.start_point_index_max,
"startPointIndex",
)?;
check(
group.id_element_value,
limits.id_element_value_max,
"idElementValue",
)?;
check(group.point_count, limits.point_count_max, "pointCount")?;
}

let prototype = groups.prototype();
let proto_len = prototype.len();

let mut section_header = CompressedVectorSectionHeader::default();
let section_offset = self.writer.physical_position()?;
section_header.section_length = CompressedVectorSectionHeader::SIZE;
section_header.write(&mut self.writer)?;
section_header.data_offset = self.writer.physical_position()?;

let max_per_packet = get_max_packet_points(&prototype);
let mut remaining = groups.groups.as_slice();
while !remaining.is_empty() {
let take = max_per_packet.min(remaining.len());
let (chunk, rest) = remaining.split_at(take);
remaining = rest;

let mut byte_streams = vec![ByteStreamWriteBuffer::new(); proto_len];
for group in chunk {
let values = [
RecordValue::Integer(group.start_point_index),
RecordValue::Integer(group.id_element_value),
RecordValue::Integer(group.point_count),
];
for (i, record) in prototype.iter().enumerate() {
record.data_type.write(&values[i], &mut byte_streams[i])?;
}
}

let mut sum_bs_sizes = 0;
let mut bs_sizes = Vec::with_capacity(proto_len);
for bs in &byte_streams {
let bs_size = bs.all_bytes();
sum_bs_sizes += bs_size;
bs_sizes.push(bs_size as u16);
}
if sum_bs_sizes == 0 {
continue;
}

let mut packet_length = DataPacketHeader::SIZE + proto_len * 2 + sum_bs_sizes;
if !packet_length.is_multiple_of(4) {
packet_length += 4 - (packet_length % 4);
}
if packet_length > u16::MAX as usize {
Error::internal("Invalid group packet length detected")?
}
section_header.section_length += packet_length as u64;

DataPacketHeader {
comp_restart_flag: false,
packet_length: packet_length as u64,
bytestream_count: proto_len as u16,
}
.write(&mut self.writer)?;

for size in bs_sizes {
self.writer
.write_all(&size.to_le_bytes())
.write_err("Cannot write group packet buffer size")?;
}
for bs in &mut byte_streams {
let data = bs.get_all_bytes();
self.writer
.write_all(&data)
.write_err("Cannot write bytestream buffer into group packet")?;
}

self.writer
.align()
.write_err("Failed to align writer after writing a group packet")?;
}

let end_offset = self
.writer
.physical_position()
.write_err("Failed to get group section end offset")?;
self.writer
.physical_seek(section_offset)
.write_err("Failed to seek to group section start for final update")?;
section_header.write(&mut self.writer)?;
self.writer
.physical_seek(end_offset)
.write_err("Failed to seek behind finalized group section")?;

Ok(PointGroupsHeader {
id_element_name: groups.id_element_name.clone(),
limits: groups.limits,
file_offset: section_offset,
records: groups.groups.len() as u64,
})
}

/// Called after all points have been added to finalize the creation of the new point cloud.
pub fn finalize(&mut self) -> Result<()> {
// Flush remaining points from buffer into byte streams and write them
Expand All @@ -621,6 +763,13 @@ impl<'a, T: Read + Write + Seek> PointCloudWriter<'a, T> {
.physical_seek(end_offset)
.write_err("Failed to seek behind finalized section")?;

// The groups, if any, go into a compressed vector section of their own,
// laid down straight after the points.
let point_groups = match self.point_groups.take() {
Some(groups) => Some(self.write_point_groups(&groups)?),
None => None,
};

// prepare point cloud metadata
let pc = PointCloud {
guid: Some(self.guid.clone()),
Expand All @@ -647,6 +796,7 @@ impl<'a, T: Read + Write + Seek> PointCloudWriter<'a, T> {
temperature: self.temperature.take(),
humidity: self.humidity.take(),
atmospheric_pressure: self.atmospheric_pressure.take(),
point_groups,
};

// Add metadata for XML generation later, when the file is completed.
Expand Down
Loading