1use crate::error::CopcError;
6
7pub const LAS_MAGIC: &[u8] = b"LASF";
9
10#[derive(Debug, Clone, PartialEq)]
12pub enum LasVersion {
13 V10,
15 V11,
17 V12,
19 V13,
21 V14,
23}
24
25impl LasVersion {
26 pub fn from_bytes(major: u8, minor: u8) -> Option<Self> {
28 match (major, minor) {
29 (1, 0) => Some(Self::V10),
30 (1, 1) => Some(Self::V11),
31 (1, 2) => Some(Self::V12),
32 (1, 3) => Some(Self::V13),
33 (1, 4) => Some(Self::V14),
34 _ => None,
35 }
36 }
37}
38
39#[derive(Debug, Clone)]
43pub struct LasHeader {
44 pub version: LasVersion,
46 pub system_id: [u8; 32],
48 pub generating_software: [u8; 32],
50 pub file_creation_day: u16,
52 pub file_creation_year: u16,
54 pub header_size: u16,
56 pub offset_to_point_data: u32,
58 pub number_of_vlrs: u32,
60 pub point_data_format_id: u8,
62 pub point_data_record_length: u16,
64 pub number_of_point_records: u64,
66 pub scale_x: f64,
68 pub scale_y: f64,
70 pub scale_z: f64,
72 pub offset_x: f64,
74 pub offset_y: f64,
76 pub offset_z: f64,
78 pub max_x: f64,
80 pub min_x: f64,
82 pub max_y: f64,
84 pub min_y: f64,
86 pub max_z: f64,
88 pub min_z: f64,
90}
91
92impl LasHeader {
93 pub fn parse(data: &[u8]) -> Result<Self, CopcError> {
102 if data.len() < 227 {
103 return Err(CopcError::InvalidFormat(format!(
104 "LAS data too short: {} bytes (need ≥ 227)",
105 data.len()
106 )));
107 }
108 if !data.starts_with(LAS_MAGIC) {
109 return Err(CopcError::InvalidFormat(
110 "Not a LAS file (bad magic)".into(),
111 ));
112 }
113
114 let major = data[24];
115 let minor = data[25];
116 let version = LasVersion::from_bytes(major, minor)
117 .ok_or(CopcError::UnsupportedVersion(major, minor))?;
118
119 let mut system_id = [0u8; 32];
120 system_id.copy_from_slice(&data[26..58]);
121 let mut generating_software = [0u8; 32];
122 generating_software.copy_from_slice(&data[58..90]);
123
124 let f64_le = |o: usize| -> f64 {
126 f64::from_le_bytes([
127 data[o],
128 data[o + 1],
129 data[o + 2],
130 data[o + 3],
131 data[o + 4],
132 data[o + 5],
133 data[o + 6],
134 data[o + 7],
135 ])
136 };
137
138 let header_size = u16::from_le_bytes([data[94], data[95]]);
139 let offset_to_point_data = u32::from_le_bytes([data[96], data[97], data[98], data[99]]);
140 let number_of_vlrs = u32::from_le_bytes([data[100], data[101], data[102], data[103]]);
141 let point_data_format_id = data[104];
142 let point_data_record_length = u16::from_le_bytes([data[105], data[106]]);
143
144 let number_of_point_records = if matches!(version, LasVersion::V14) && data.len() >= 255 {
147 u64::from_le_bytes([
148 data[247], data[248], data[249], data[250], data[251], data[252], data[253],
149 data[254],
150 ])
151 } else {
152 u32::from_le_bytes([data[107], data[108], data[109], data[110]]) as u64
153 };
154
155 Ok(Self {
156 version,
157 system_id,
158 generating_software,
159 file_creation_day: u16::from_le_bytes([data[90], data[91]]),
160 file_creation_year: u16::from_le_bytes([data[92], data[93]]),
161 header_size,
162 offset_to_point_data,
163 number_of_vlrs,
164 point_data_format_id,
165 point_data_record_length,
166 number_of_point_records,
167 scale_x: f64_le(131),
168 scale_y: f64_le(139),
169 scale_z: f64_le(147),
170 offset_x: f64_le(155),
171 offset_y: f64_le(163),
172 offset_z: f64_le(171),
173 max_x: f64_le(179),
174 min_x: f64_le(187),
175 max_y: f64_le(195),
176 min_y: f64_le(203),
177 max_z: f64_le(211),
178 min_z: f64_le(219),
179 })
180 }
181
182 pub fn bounds(&self) -> ([f64; 3], [f64; 3]) {
184 (
185 [self.min_x, self.min_y, self.min_z],
186 [self.max_x, self.max_y, self.max_z],
187 )
188 }
189}