pub mod arithmetic_decoder;
pub mod arithmetic_encoder;
pub mod arithmetic_model;
pub mod chunk;
pub mod codec;
pub mod fields;
pub mod integer_codec;
pub mod laszip_chunk_table;
pub mod reader;
pub mod standard_point10;
pub mod standard_point10_write;
pub mod standard_point14;
pub mod writer;
pub use reader::LazReader;
pub use writer::{LazWriter, LazWriterConfig};
use crate::las::header::PointDataFormat;
use crate::las::vlr::{Vlr, VlrKey};
pub const LASZIP_USER_ID: &str = "laszip encoded";
pub const LASZIP_RECORD_ID: u16 = 22204;
pub const DEFAULT_CHUNK_SIZE: u32 = 50_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LaszipCompressorType {
None,
PointWise,
PointWiseChunked,
LayeredChunked,
Unknown(u16),
}
impl LaszipCompressorType {
fn from_u16(value: u16) -> Self {
match value {
0 => Self::None,
1 => Self::PointWise,
2 => Self::PointWiseChunked,
3 => Self::LayeredChunked,
other => Self::Unknown(other),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LaszipItemSpec {
pub item_type: u16,
pub item_size: u16,
pub item_version: u16,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LaszipVlrInfo {
pub compressor: LaszipCompressorType,
pub coder: u16,
pub chunk_size: u32,
pub items: Vec<LaszipItemSpec>,
}
impl LaszipVlrInfo {
pub fn uses_arithmetic_coder(&self) -> bool {
self.coder == 0
}
pub fn has_point14_item(&self) -> bool {
self.items.iter().any(|it| it.item_type == 10)
}
pub fn has_point10_item(&self) -> bool {
self.items.iter().any(|it| it.item_type == 6)
}
pub fn has_rgb14_item(&self) -> bool {
self.items.iter().any(|it| it.item_type == 11)
}
pub fn has_rgbnir14_item(&self) -> bool {
self.items.iter().any(|it| it.item_type == 12)
}
pub fn has_nir14_item(&self) -> bool {
self.has_rgbnir14_item()
}
}
pub fn parse_laszip_vlr(vlrs: &[crate::las::Vlr]) -> Option<LaszipVlrInfo> {
let vlr = vlrs.iter().find(|v| {
v.key.user_id == LASZIP_USER_ID && v.key.record_id == LASZIP_RECORD_ID
})?;
if vlr.data.len() < 34 {
return None;
}
let compressor_raw = u16::from_le_bytes([vlr.data[0], vlr.data[1]]);
let coder = u16::from_le_bytes([vlr.data[2], vlr.data[3]]);
let chunk_size = u32::from_le_bytes([vlr.data[12], vlr.data[13], vlr.data[14], vlr.data[15]]);
let num_items = u16::from_le_bytes([vlr.data[32], vlr.data[33]]) as usize;
let expected_items_bytes = num_items.checked_mul(6)?;
let expected_total = 34usize.checked_add(expected_items_bytes)?;
if vlr.data.len() < expected_total {
return None;
}
let mut items = Vec::with_capacity(num_items);
let mut offset = 34usize;
for _ in 0..num_items {
let item_type = u16::from_le_bytes([vlr.data[offset], vlr.data[offset + 1]]);
let item_size = u16::from_le_bytes([vlr.data[offset + 2], vlr.data[offset + 3]]);
let item_version = u16::from_le_bytes([vlr.data[offset + 4], vlr.data[offset + 5]]);
items.push(LaszipItemSpec {
item_type,
item_size,
item_version,
});
offset += 6;
}
Some(LaszipVlrInfo {
compressor: LaszipCompressorType::from_u16(compressor_raw),
coder,
chunk_size,
items,
})
}
pub fn parse_vlr_chunk_size(vlrs: &[crate::las::Vlr]) -> Option<u32> {
parse_laszip_vlr(vlrs).map(|v| v.chunk_size)
}
pub fn build_laszip_vlr_for_format(point_data_format: PointDataFormat, chunk_size: u32) -> Vlr {
build_laszip_vlr_for_format_with_extra_bytes(point_data_format, chunk_size, 0)
}
pub fn build_laszip_vlr_for_format_with_extra_bytes(
point_data_format: PointDataFormat,
chunk_size: u32,
extra_bytes_per_point: u16,
) -> Vlr {
let mut data = Vec::with_capacity(64);
let is_point14_family = point_data_format.is_v14() || point_data_format.is_v15();
let compressor = if is_point14_family { 3u16 } else { 2u16 };
data.extend_from_slice(&compressor.to_le_bytes());
data.extend_from_slice(&0u16.to_le_bytes());
data.push(2);
data.push(2);
data.extend_from_slice(&0u16.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&chunk_size.to_le_bytes());
data.extend_from_slice(&(-1i64).to_le_bytes());
data.extend_from_slice(&(-1i64).to_le_bytes());
let waveform_bytes_per_point = if point_data_format.has_waveform() {
29u16
} else {
0u16
};
let mut items: Vec<(u16, u16, u16)> = if is_point14_family {
vec![(10, 30, 3)]
} else {
vec![(6, 20, 2)]
};
if is_point14_family {
if point_data_format.has_rgb() {
if point_data_format.has_nir() {
items.push((12, 8, 3));
} else {
items.push((11, 6, 3));
}
}
let byte14_total = extra_bytes_per_point.saturating_add(waveform_bytes_per_point);
if byte14_total > 0 {
items.push((14, byte14_total, 3));
}
} else {
if point_data_format.has_gps_time() {
items.push((7, 8, 2));
}
if point_data_format.has_rgb() {
items.push((8, 6, 2));
}
let byte_total = extra_bytes_per_point.saturating_add(waveform_bytes_per_point);
if byte_total > 0 {
items.push((0, byte_total, 2));
}
}
data.extend_from_slice(&(items.len() as u16).to_le_bytes());
for (item_type, item_size, item_version) in items {
data.extend_from_slice(&item_type.to_le_bytes());
data.extend_from_slice(&item_size.to_le_bytes());
data.extend_from_slice(&item_version.to_le_bytes());
}
Vlr {
key: VlrKey {
user_id: LASZIP_USER_ID.to_owned(),
record_id: LASZIP_RECORD_ID,
},
description: "LASzip by Martin Isenburg".to_owned(),
data,
extended: false,
}
}
#[cfg(test)]
mod tests {
use super::{
build_laszip_vlr_for_format,
build_laszip_vlr_for_format_with_extra_bytes,
parse_laszip_vlr,
LaszipCompressorType,
};
use crate::las::header::PointDataFormat;
use crate::las::vlr::{Vlr, VlrKey};
use crate::laz::{LASZIP_RECORD_ID, LASZIP_USER_ID};
fn build_test_laszip_vlr_data() -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&2u16.to_le_bytes()); data.extend_from_slice(&0u16.to_le_bytes()); data.push(2); data.push(2); data.extend_from_slice(&0u16.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&50_000u32.to_le_bytes()); data.extend_from_slice(&(-1i64).to_le_bytes()); data.extend_from_slice(&(-1i64).to_le_bytes()); data.extend_from_slice(&2u16.to_le_bytes()); data.extend_from_slice(&10u16.to_le_bytes());
data.extend_from_slice(&30u16.to_le_bytes());
data.extend_from_slice(&3u16.to_le_bytes());
data.extend_from_slice(&11u16.to_le_bytes());
data.extend_from_slice(&6u16.to_le_bytes());
data.extend_from_slice(&3u16.to_le_bytes());
data
}
#[test]
fn parses_laszip_vlr_core_fields() {
let vlrs = vec![Vlr {
key: VlrKey {
user_id: LASZIP_USER_ID.to_string(),
record_id: LASZIP_RECORD_ID,
},
description: "LASzip by Martin Isenburg".to_string(),
data: build_test_laszip_vlr_data(),
extended: false,
}];
let parsed = parse_laszip_vlr(&vlrs).expect("expected parsed LASzip VLR");
assert_eq!(parsed.compressor, LaszipCompressorType::PointWiseChunked);
assert!(parsed.uses_arithmetic_coder());
assert_eq!(parsed.chunk_size, 50_000);
assert_eq!(parsed.items.len(), 2);
assert_eq!(parsed.items[0].item_type, 10);
assert_eq!(parsed.items[1].item_type, 11);
assert!(!parsed.has_point10_item());
assert!(parsed.has_point14_item());
assert!(parsed.has_rgb14_item());
assert!(!parsed.has_nir14_item());
}
#[test]
fn rejects_truncated_laszip_vlr() {
let vlrs = vec![Vlr {
key: VlrKey {
user_id: LASZIP_USER_ID.to_string(),
record_id: LASZIP_RECORD_ID,
},
description: "truncated".to_string(),
data: vec![0u8; 20],
extended: false,
}];
assert!(parse_laszip_vlr(&vlrs).is_none());
}
#[test]
fn builds_point14_family_laszip_vlr_for_pdrf8() {
let vlr = build_laszip_vlr_for_format(PointDataFormat::Pdrf8, 50_000);
let parsed = parse_laszip_vlr(&[vlr]).expect("expected parsed LASzip VLR");
assert_eq!(parsed.compressor, LaszipCompressorType::LayeredChunked);
assert!(parsed.has_point14_item());
assert!(!parsed.has_rgb14_item());
assert!(parsed.has_rgbnir14_item());
assert!(parsed.has_nir14_item());
}
#[test]
fn builds_point10_family_laszip_vlr_for_pdrf3_with_extra_bytes() {
let vlr = build_laszip_vlr_for_format_with_extra_bytes(PointDataFormat::Pdrf3, 50_000, 2);
let parsed = parse_laszip_vlr(&[vlr]).expect("expected parsed LASzip VLR");
assert_eq!(parsed.compressor, LaszipCompressorType::PointWiseChunked);
assert!(parsed.has_point10_item());
assert!(parsed.items.iter().any(|item| item.item_type == 7 && item.item_size == 8));
assert!(parsed.items.iter().any(|item| item.item_type == 8 && item.item_size == 6));
assert!(parsed.items.iter().any(|item| item.item_type == 0 && item.item_size == 2));
}
}