Skip to main content

oxigdal_copc/
las_header.rs

1//! ASPRS LAS 1.x file header parser.
2//!
3//! Reference: ASPRS LAS Specification 1.4 R15 (November 2019).
4
5use crate::error::CopcError;
6
7/// The four-byte magic that must appear at the start of every LAS file.
8pub const LAS_MAGIC: &[u8] = b"LASF";
9
10/// LAS format version.
11#[derive(Debug, Clone, PartialEq)]
12pub enum LasVersion {
13    /// LAS 1.0
14    V10,
15    /// LAS 1.1
16    V11,
17    /// LAS 1.2
18    V12,
19    /// LAS 1.3
20    V13,
21    /// LAS 1.4
22    V14,
23}
24
25impl LasVersion {
26    /// Construct from major/minor version bytes.
27    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/// Parsed LAS public header block.
40///
41/// Field offsets and sizes are taken from the LAS 1.4 specification.
42#[derive(Debug, Clone)]
43pub struct LasHeader {
44    /// LAS format version.
45    pub version: LasVersion,
46    /// System identifier (32 bytes, null-padded ASCII).
47    pub system_id: [u8; 32],
48    /// Generating software string (32 bytes, null-padded ASCII).
49    pub generating_software: [u8; 32],
50    /// File creation day-of-year.
51    pub file_creation_day: u16,
52    /// File creation year.
53    pub file_creation_year: u16,
54    /// Size of the public header block in bytes.
55    pub header_size: u16,
56    /// Byte offset from the start of the file to point data.
57    pub offset_to_point_data: u32,
58    /// Number of variable length records.
59    pub number_of_vlrs: u32,
60    /// Point data format ID (0–10).
61    pub point_data_format_id: u8,
62    /// Size of a single point record in bytes.
63    pub point_data_record_length: u16,
64    /// Total number of point records in the file.
65    pub number_of_point_records: u64,
66    /// Scale factor applied to raw X integer coordinates.
67    pub scale_x: f64,
68    /// Scale factor applied to raw Y integer coordinates.
69    pub scale_y: f64,
70    /// Scale factor applied to raw Z integer coordinates.
71    pub scale_z: f64,
72    /// X coordinate offset.
73    pub offset_x: f64,
74    /// Y coordinate offset.
75    pub offset_y: f64,
76    /// Z coordinate offset.
77    pub offset_z: f64,
78    /// Maximum X value.
79    pub max_x: f64,
80    /// Minimum X value.
81    pub min_x: f64,
82    /// Maximum Y value.
83    pub max_y: f64,
84    /// Minimum Y value.
85    pub min_y: f64,
86    /// Maximum Z value.
87    pub max_z: f64,
88    /// Minimum Z value.
89    pub min_z: f64,
90}
91
92impl LasHeader {
93    /// Parse a LAS public header from a byte slice.
94    ///
95    /// The slice must contain at least 227 bytes (the LAS 1.0–1.3 header size).
96    ///
97    /// # Errors
98    /// Returns [`CopcError::InvalidFormat`] when the data is too short or the
99    /// magic is wrong, and [`CopcError::UnsupportedVersion`] for unknown version
100    /// bytes.
101    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        // Helper: read a little-endian f64 at byte offset `o`.
125        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        // LAS 1.4 stores the 64-bit point count at offset 247 (header ≥ 375 bytes).
145        // For earlier versions we read the 32-bit legacy field at offset 107.
146        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    /// Return the bounding box as `(min, max)` in (X, Y, Z) order.
183    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}