use crate::error::CopcError;
pub const LAS_MAGIC: &[u8] = b"LASF";
#[derive(Debug, Clone, PartialEq)]
pub enum LasVersion {
V10,
V11,
V12,
V13,
V14,
}
impl LasVersion {
pub fn from_bytes(major: u8, minor: u8) -> Option<Self> {
match (major, minor) {
(1, 0) => Some(Self::V10),
(1, 1) => Some(Self::V11),
(1, 2) => Some(Self::V12),
(1, 3) => Some(Self::V13),
(1, 4) => Some(Self::V14),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct LasHeader {
pub version: LasVersion,
pub system_id: [u8; 32],
pub generating_software: [u8; 32],
pub file_creation_day: u16,
pub file_creation_year: u16,
pub header_size: u16,
pub offset_to_point_data: u32,
pub number_of_vlrs: u32,
pub point_data_format_id: u8,
pub point_data_record_length: u16,
pub number_of_point_records: u64,
pub scale_x: f64,
pub scale_y: f64,
pub scale_z: f64,
pub offset_x: f64,
pub offset_y: f64,
pub offset_z: f64,
pub max_x: f64,
pub min_x: f64,
pub max_y: f64,
pub min_y: f64,
pub max_z: f64,
pub min_z: f64,
}
impl LasHeader {
pub fn parse(data: &[u8]) -> Result<Self, CopcError> {
if data.len() < 227 {
return Err(CopcError::InvalidFormat(format!(
"LAS data too short: {} bytes (need ≥ 227)",
data.len()
)));
}
if !data.starts_with(LAS_MAGIC) {
return Err(CopcError::InvalidFormat(
"Not a LAS file (bad magic)".into(),
));
}
let major = data[24];
let minor = data[25];
let version = LasVersion::from_bytes(major, minor)
.ok_or(CopcError::UnsupportedVersion(major, minor))?;
let mut system_id = [0u8; 32];
system_id.copy_from_slice(&data[26..58]);
let mut generating_software = [0u8; 32];
generating_software.copy_from_slice(&data[58..90]);
let f64_le = |o: usize| -> f64 {
f64::from_le_bytes([
data[o],
data[o + 1],
data[o + 2],
data[o + 3],
data[o + 4],
data[o + 5],
data[o + 6],
data[o + 7],
])
};
let header_size = u16::from_le_bytes([data[94], data[95]]);
let offset_to_point_data = u32::from_le_bytes([data[96], data[97], data[98], data[99]]);
let number_of_vlrs = u32::from_le_bytes([data[100], data[101], data[102], data[103]]);
let point_data_format_id = data[104];
let point_data_record_length = u16::from_le_bytes([data[105], data[106]]);
let number_of_point_records = if matches!(version, LasVersion::V14) && data.len() >= 255 {
u64::from_le_bytes([
data[247], data[248], data[249], data[250], data[251], data[252], data[253],
data[254],
])
} else {
u32::from_le_bytes([data[107], data[108], data[109], data[110]]) as u64
};
Ok(Self {
version,
system_id,
generating_software,
file_creation_day: u16::from_le_bytes([data[90], data[91]]),
file_creation_year: u16::from_le_bytes([data[92], data[93]]),
header_size,
offset_to_point_data,
number_of_vlrs,
point_data_format_id,
point_data_record_length,
number_of_point_records,
scale_x: f64_le(131),
scale_y: f64_le(139),
scale_z: f64_le(147),
offset_x: f64_le(155),
offset_y: f64_le(163),
offset_z: f64_le(171),
max_x: f64_le(179),
min_x: f64_le(187),
max_y: f64_le(195),
min_y: f64_le(203),
max_z: f64_le(211),
min_z: f64_le(219),
})
}
pub fn bounds(&self) -> ([f64; 3], [f64; 3]) {
(
[self.min_x, self.min_y, self.min_z],
[self.max_x, self.max_y, self.max_z],
)
}
}