1use std::fs::File;
4use std::io::{BufReader, BufWriter, Seek, SeekFrom, Write};
5use std::path::Path;
6
7use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
8use copc_core::{
9 Bounds, CancelCheck, CopcInfo, Entry, Error, LasPointRecord, NeverCancel, Result,
10 StreamingLayout, VoxelKey, MAX_EVLR_COUNT, MAX_VLR_COUNT,
11};
12use las::point::Format as LasFormat;
13use laz::{LasZipCompressor, LazVlrBuilder};
14use tempfile::NamedTempFile;
15
16use crate::hierarchy_pages::{
17 assign_hierarchy_page_offsets, plan_hierarchy_pages, write_hierarchy_page_tree,
18};
19use crate::las_out::{
20 regular_las_vlrs_bytes, write_evlr_header, write_las_evlr, write_las_vlr, write_vlr_header,
21 LasHeader, LAS_EVLR_HEADER_BYTES, LAS_VLR_HEADER_BYTES,
22};
23use crate::lod::{build_lod_index, cube_from_bounds, INDEX_IO_BUFFER_BYTES};
24use crate::metadata::{
25 read_all_source_evlrs, CopcWriteMetadata, OutputLasMetadata, LASZIP_VLR_RECORD_ID,
26 LASZIP_VLR_USER_ID,
27};
28use crate::source::{CopcPointFields, CopcPointSource, SpillSource};
29use crate::spill::{SpillReader, SpillWriter};
30use crate::validate::{
31 quantize_xyz, scan_angle_to_las_scaled, validate_las_conversion_supported,
32 validate_source_points, validate_streaming_layout_supported, validate_write_setup, PointStats,
33};
34use crate::CANCEL_POLL_STRIDE;
35
36const LAS_INPUT_BUFFER_BYTES: usize = 1024 * 1024;
37const COPC_OUTPUT_BUFFER_BYTES: usize = 1024 * 1024;
38const LAS_POINT_BATCH_SIZE: u64 = 64 * 1024;
39
40#[derive(Debug, Clone, Copy)]
42#[non_exhaustive]
43pub struct CopcWriterParams {
44 pub max_points_per_node: u32,
48}
49
50impl CopcWriterParams {
51 pub fn new(max_points_per_node: u32) -> Self {
52 Self {
53 max_points_per_node,
54 }
55 }
56}
57
58impl Default for CopcWriterParams {
59 fn default() -> Self {
60 Self::new(100_000)
61 }
62}
63
64pub fn write_source<S: CopcPointSource>(
65 path: &Path,
66 source: &S,
67 has_color: bool,
68 bounds: Bounds,
69 params: &CopcWriterParams,
70 metadata: &CopcWriteMetadata,
71) -> Result<()> {
72 write_source_with_cancel(
73 path,
74 source,
75 has_color,
76 bounds,
77 params,
78 metadata,
79 &NeverCancel,
80 )
81}
82
83pub fn write_source_with_cancel<S: CopcPointSource>(
84 path: &Path,
85 source: &S,
86 has_color: bool,
87 bounds: Bounds,
88 params: &CopcWriterParams,
89 metadata: &CopcWriteMetadata,
90 cancel: &dyn CancelCheck,
91) -> Result<()> {
92 cancel.check()?;
93 if source.is_empty() {
94 return Err(Error::InvalidInput(
95 "cannot write empty cloud to COPC".into(),
96 ));
97 }
98 write_copc_inner(
99 path,
100 source,
101 has_color,
102 bounds,
103 params,
104 cancel,
105 &metadata.to_output(),
106 None,
107 )
108}
109
110pub fn write_streaming_with_cancel<I>(
111 path: &Path,
112 layout: StreamingLayout,
113 points: I,
114 params: &CopcWriterParams,
115 metadata: &CopcWriteMetadata,
116 spill_dir: &Path,
117 cancel: &dyn CancelCheck,
118) -> Result<()>
119where
120 I: IntoIterator<Item = Result<LasPointRecord>>,
121{
122 cancel.check()?;
123 validate_streaming_layout_supported(&layout)?;
124 let mut spill = SpillWriter::create(spill_dir, layout)?;
125 for (index, item) in points.into_iter().enumerate() {
126 if index.is_multiple_of(CANCEL_POLL_STRIDE) {
127 cancel.check()?;
128 }
129 spill.push(&item?)?;
130 }
131 cancel.check()?;
132 let reader = spill.finalize()?;
133 write_copc_from_spill(path, reader, params, cancel, &metadata.to_output())
134}
135
136pub fn convert_las_to_copc_streaming(
137 las_path: &Path,
138 copc_path: &Path,
139 params: &CopcWriterParams,
140 spill_dir: &Path,
141 cancel: &dyn CancelCheck,
142) -> Result<()> {
143 convert_las_to_copc_streaming_inner(las_path, copc_path, params, spill_dir, cancel, None)
144}
145
146pub fn convert_las_to_copc_streaming_with_crs_wkt_override(
149 las_path: &Path,
150 copc_path: &Path,
151 params: &CopcWriterParams,
152 spill_dir: &Path,
153 cancel: &dyn CancelCheck,
154 crs_wkt_override: Option<&str>,
155) -> Result<()> {
156 convert_las_to_copc_streaming_inner(
157 las_path,
158 copc_path,
159 params,
160 spill_dir,
161 cancel,
162 crs_wkt_override,
163 )
164}
165
166fn convert_las_to_copc_streaming_inner(
167 las_path: &Path,
168 copc_path: &Path,
169 params: &CopcWriterParams,
170 spill_dir: &Path,
171 cancel: &dyn CancelCheck,
172 crs_wkt_override: Option<&str>,
173) -> Result<()> {
174 cancel.check()?;
175 let las_file = File::open(las_path).map_err(|e| Error::io("open source LAS/LAZ", e))?;
176 let mut reader = las::Reader::new(BufReader::with_capacity(LAS_INPUT_BUFFER_BYTES, las_file))
177 .map_err(|e| Error::Las(e.to_string()))?;
178 let source_evlrs = read_all_source_evlrs(las_path)?;
179 validate_las_conversion_supported(reader.header(), &source_evlrs, crs_wkt_override)?;
180 let output_metadata =
181 OutputLasMetadata::from_las_header(reader.header(), &source_evlrs, crs_wkt_override);
182 let layout = StreamingLayout::from_las_header(reader.header());
183 let mut spill = SpillWriter::create(spill_dir, layout)?;
184 let mut point_data = las::PointDataBuilder::new()
185 .for_header(reader.header())
186 .build();
187 let mut index = 0usize;
188 loop {
189 let count = reader
190 .fill_points(LAS_POINT_BATCH_SIZE, &mut point_data)
191 .map_err(|e| Error::Las(e.to_string()))?;
192 if count == 0 {
193 break;
194 }
195 for result in point_data.points() {
196 if index.is_multiple_of(CANCEL_POLL_STRIDE) {
197 cancel.check()?;
198 }
199 let point = result.map_err(|e| Error::Las(e.to_string()))?;
200 spill.push(&LasPointRecord::from_las_point(&point))?;
201 index = index
202 .checked_add(1)
203 .ok_or_else(|| Error::InvalidInput("source point count exceeds usize".into()))?;
204 }
205 }
206 cancel.check()?;
207 let reader = spill.finalize()?;
208 write_copc_from_spill(copc_path, reader, params, cancel, &output_metadata)
209}
210
211fn write_copc_from_spill(
212 path: &Path,
213 reader: SpillReader,
214 params: &CopcWriterParams,
215 cancel: &dyn CancelCheck,
216 metadata: &OutputLasMetadata,
217) -> Result<()> {
218 cancel.check()?;
219 if params.max_points_per_node == 0 {
220 return Err(Error::InvalidInput(
221 "max_points_per_node must be greater than zero".into(),
222 ));
223 }
224 validate_streaming_layout_supported(reader.layout())?;
225 if reader.is_empty() {
226 return Err(Error::InvalidInput(
227 "cannot write empty cloud to COPC".into(),
228 ));
229 }
230 let has_color = reader.layout().has_color;
231 let bounds = reader.bounds();
232 let stats = reader.stats();
233 let source = SpillSource::new(&reader);
234 write_copc_inner(
235 path,
236 &source,
237 has_color,
238 bounds,
239 params,
240 cancel,
241 metadata,
242 Some(stats),
243 )
244}
245
246#[allow(clippy::too_many_arguments)]
247fn write_copc_inner<S: CopcPointSource>(
248 path: &Path,
249 source: &S,
250 has_color: bool,
251 bounds: Bounds,
252 params: &CopcWriterParams,
253 cancel: &dyn CancelCheck,
254 metadata: &OutputLasMetadata,
255 intake_stats: Option<PointStats>,
256) -> Result<()> {
257 cancel.check()?;
258 if params.max_points_per_node == 0 {
259 return Err(Error::InvalidInput(
260 "max_points_per_node must be greater than zero".into(),
261 ));
262 }
263 let point_format_id = if has_color { 7u8 } else { 6u8 };
264 let mut point_format =
265 LasFormat::new(point_format_id).map_err(|e| Error::Las(format!("point format: {e}")))?;
266 let extra_byte_count = source.extra_byte_count();
267 let point_record_length = point_format
268 .len()
269 .checked_add(extra_byte_count)
270 .ok_or_else(|| {
271 Error::InvalidInput(format!(
272 "point record length with {extra_byte_count} extra bytes exceeds LAS u16 range"
273 ))
274 })?;
275 point_format.extra_bytes = extra_byte_count;
276
277 let (scale_x, scale_y, scale_z) = metadata.scale;
278 let (offset_x, offset_y, offset_z) =
279 metadata
280 .offset
281 .unwrap_or((bounds.min.0, bounds.min.1, bounds.min.2));
282 validate_write_setup(
283 bounds,
284 (scale_x, scale_y, scale_z),
285 (offset_x, offset_y, offset_z),
286 )?;
287 let point_stats = match intake_stats {
292 Some(stats) => stats,
293 None => validate_source_points(
294 source,
295 bounds,
296 (scale_x, scale_y, scale_z),
297 (offset_x, offset_y, offset_z),
298 cancel,
299 )?,
300 };
301 let (center, halfsize) = cube_from_bounds(&bounds);
302
303 let lod_index = build_lod_index(source, center, halfsize, params, cancel)?;
304 cancel.check()?;
305
306 let var_vlr = LazVlrBuilder::default()
307 .with_point_format(point_format_id, extra_byte_count)
308 .map_err(|e| Error::Las(format!("laz items: {e}")))?
309 .with_variable_chunk_size()
310 .build();
311 let mut var_vlr_bytes = Vec::new();
312 var_vlr
313 .write_to(&mut var_vlr_bytes)
314 .map_err(|e| Error::Las(format!("variable chunk LAZ VLR: {e}")))?;
315
316 let copc_info_vlr_size = 160u16;
317 let las_header_size = 375u32;
318 let regular_crs_vlr_count = metadata.regular_crs_vlr_count();
319 let regular_crs_vlr_bytes = metadata.regular_crs_vlr_bytes()?;
320 let extra_bytes_vlrs = source.extra_bytes_vlrs();
321 let extra_bytes_vlr_bytes = regular_las_vlrs_bytes(extra_bytes_vlrs)?;
322 let pass_through_vlr_bytes = regular_las_vlrs_bytes(&metadata.pass_through_vlrs)?;
323 let number_of_vlrs = u32::try_from(
324 2usize
325 .checked_add(regular_crs_vlr_count)
326 .and_then(|count| count.checked_add(extra_bytes_vlrs.len()))
327 .and_then(|count| count.checked_add(metadata.pass_through_vlrs.len()))
328 .ok_or_else(|| Error::InvalidInput("VLR count overflow".into()))?,
329 )
330 .map_err(|_| Error::InvalidInput("VLR count overflow".into()))?;
331 if number_of_vlrs > MAX_VLR_COUNT {
332 return Err(Error::InvalidInput(format!(
333 "output VLR count {number_of_vlrs} exceeds max supported {MAX_VLR_COUNT}"
334 )));
335 }
336 let number_of_evlrs = u32::try_from(
337 1usize
338 .checked_add(metadata.source_evlr_count_after_hierarchy())
339 .ok_or_else(|| Error::InvalidInput("EVLR count overflow".into()))?,
340 )
341 .map_err(|_| Error::InvalidInput("EVLR count overflow".into()))?;
342 if number_of_evlrs > MAX_EVLR_COUNT {
343 return Err(Error::InvalidInput(format!(
344 "output EVLR count {number_of_evlrs} exceeds max supported {MAX_EVLR_COUNT}"
345 )));
346 }
347 let var_vlr_body_size = u16::try_from(var_vlr_bytes.len())
348 .map_err(|_| Error::InvalidInput("LAZ VLR byte size exceeds LAS VLR limit".into()))?;
349 let var_vlr_storage_bytes = LAS_VLR_HEADER_BYTES
350 .checked_add(u32::from(var_vlr_body_size))
351 .ok_or_else(|| Error::InvalidInput("LAZ VLR byte size overflow".into()))?;
352 let total_vlr_bytes = LAS_VLR_HEADER_BYTES
353 .checked_add(u32::from(copc_info_vlr_size))
354 .and_then(|total| total.checked_add(var_vlr_storage_bytes))
355 .and_then(|total| total.checked_add(regular_crs_vlr_bytes))
356 .and_then(|total| total.checked_add(extra_bytes_vlr_bytes))
357 .and_then(|total| total.checked_add(pass_through_vlr_bytes))
358 .ok_or_else(|| Error::InvalidInput("VLR byte size overflow".into()))?;
359 let offset_to_point_data = las_header_size
360 .checked_add(total_vlr_bytes)
361 .ok_or_else(|| Error::InvalidInput("point data offset overflow".into()))?;
362
363 let pending = PendingOutput::create(path)?;
364 let file = pending.reopen()?;
365 let mut writer = BufWriter::with_capacity(COPC_OUTPUT_BUFFER_BYTES, file);
366
367 let header = LasHeader {
368 point_data_format: point_format_id | 0x80,
369 point_record_length,
370 offset_to_point_data,
371 number_of_vlrs,
372 file_source_id: metadata.file_source_id,
373 global_encoding: metadata.global_encoding,
374 guid: metadata.guid,
375 system_identifier: metadata.system_identifier.clone(),
376 generating_software: metadata.generating_software.clone(),
377 creation_day_of_year: metadata.creation_day_of_year,
378 creation_year: metadata.creation_year,
379 scale: (scale_x, scale_y, scale_z),
380 offset: (offset_x, offset_y, offset_z),
381 bounds,
382 legacy_point_count: 0,
383 total_point_count: source.len() as u64,
384 offset_to_first_evlr: 0,
385 number_of_evlrs,
386 extended_return_counts: point_stats.extended_return_counts,
387 };
388 header.write(&mut writer)?;
389
390 write_vlr_header(&mut writer, "copc", 1, copc_info_vlr_size, "COPC info")?;
391 let copc_info_payload_start = writer
392 .stream_position()
393 .map_err(|e| Error::io("record COPC info payload offset", e))?;
394 writer
395 .write_all(&[0u8; 160])
396 .map_err(|e| Error::io("write COPC info placeholder", e))?;
397
398 write_vlr_header(
399 &mut writer,
400 LASZIP_VLR_USER_ID,
401 LASZIP_VLR_RECORD_ID,
402 var_vlr_body_size,
403 "http://laszip.org",
404 )?;
405 writer
406 .write_all(&var_vlr_bytes)
407 .map_err(|e| Error::io("write LAZ VLR", e))?;
408
409 for vlr in metadata.regular_crs_vlrs() {
410 write_las_vlr(&mut writer, vlr)?;
411 }
412 for vlr in extra_bytes_vlrs {
413 write_las_vlr(&mut writer, vlr)?;
414 }
415 for vlr in &metadata.pass_through_vlrs {
416 write_las_vlr(&mut writer, vlr)?;
417 }
418
419 let point_data_actual_start = writer
420 .stream_position()
421 .map_err(|e| Error::io("record point data offset", e))?;
422 if point_data_actual_start as u32 != offset_to_point_data {
423 return Err(Error::InvalidInput(format!(
424 "VLR size accounting mismatch: at {point_data_actual_start}, expected {offset_to_point_data}"
425 )));
426 }
427
428 let hierarchy = compress_nodes(
429 &mut writer,
430 &var_vlr,
431 &lod_index,
432 source,
433 (scale_x, scale_y, scale_z),
434 (offset_x, offset_y, offset_z),
435 usize::from(point_record_length),
436 &point_format,
437 cancel,
438 )?;
439
440 let hierarchy_evlr_start = writer
441 .stream_position()
442 .map_err(|e| Error::io("record hierarchy EVLR start", e))?;
443 let root_hier_offset = hierarchy_evlr_start
444 .checked_add(LAS_EVLR_HEADER_BYTES)
445 .ok_or_else(|| Error::InvalidInput("hierarchy EVLR offset overflow".into()))?;
446 let mut hierarchy_pages = plan_hierarchy_pages(&hierarchy, VoxelKey::root())?;
447 let hierarchy_end = assign_hierarchy_page_offsets(&mut hierarchy_pages, root_hier_offset)?;
448 let hierarchy_body_size = hierarchy_end
449 .checked_sub(root_hier_offset)
450 .ok_or_else(|| Error::InvalidInput("hierarchy size overflow".into()))?;
451 write_evlr_header(
452 &mut writer,
453 "copc",
454 1000,
455 hierarchy_body_size,
456 "COPC hierarchy",
457 )?;
458 let actual_root_hier_offset = writer
459 .stream_position()
460 .map_err(|e| Error::io("record root hierarchy offset", e))?;
461 if actual_root_hier_offset != root_hier_offset {
462 return Err(Error::InvalidInput(format!(
463 "hierarchy offset accounting mismatch: at {actual_root_hier_offset}, expected {root_hier_offset}"
464 )));
465 }
466 write_hierarchy_page_tree(&mut writer, &hierarchy_pages)?;
467 for evlr in metadata.source_evlrs_after_hierarchy() {
468 write_las_evlr(&mut writer, evlr)?;
469 }
470
471 writer
472 .seek(SeekFrom::Start(copc_info_payload_start))
473 .map_err(|e| Error::io("seek COPC info payload", e))?;
474 let info = CopcInfo {
475 center,
476 halfsize,
477 spacing: halfsize / 128.0,
478 root_hier_offset,
479 root_hier_size: hierarchy_pages.byte_size,
480 gpstime_min: point_stats.gpstime_min,
481 gpstime_max: point_stats.gpstime_max,
482 };
483 let info_bytes = info.write_le_bytes()?;
484 writer
485 .write_all(&info_bytes)
486 .map_err(|e| Error::io("patch COPC info", e))?;
487
488 writer
489 .seek(SeekFrom::Start(235))
490 .map_err(|e| Error::io("seek first EVLR offset", e))?;
491 writer
492 .write_u64::<LittleEndian>(hierarchy_evlr_start)
493 .map_err(|e| Error::io("patch first EVLR offset", e))?;
494
495 writer
496 .flush()
497 .map_err(|e| Error::io("flush COPC file", e))?;
498 let file = writer
499 .into_inner()
500 .map_err(|e| Error::io("flush COPC file", e.into_error()))?;
501 file.sync_all()
502 .map_err(|e| Error::io("sync COPC file", e))?;
503 drop(file);
504 pending.commit()?;
505 Ok(())
506}
507
508struct PendingOutput {
512 file: Option<NamedTempFile>,
513 final_path: std::path::PathBuf,
514}
515
516impl PendingOutput {
517 fn create(path: &Path) -> Result<Self> {
518 let file_name = path.file_name().ok_or_else(|| {
519 Error::InvalidInput(format!("output path {} has no file name", path.display()))
520 })?;
521 let mut prefix = std::ffi::OsString::from(".");
522 prefix.push(file_name);
523 prefix.push(".");
524 let parent = path
525 .parent()
526 .filter(|parent| !parent.as_os_str().is_empty())
527 .unwrap_or_else(|| Path::new("."));
528 let file = tempfile::Builder::new()
529 .prefix(&prefix)
530 .suffix(".part")
531 .tempfile_in(parent)
532 .map_err(|e| Error::io("create temporary COPC file", e))?;
533 Ok(Self {
534 file: Some(file),
535 final_path: path.to_path_buf(),
536 })
537 }
538
539 fn reopen(&self) -> Result<File> {
540 self.file
541 .as_ref()
542 .ok_or_else(|| Error::InvalidInput("temporary COPC file already committed".into()))?
543 .reopen()
544 .map_err(|e| Error::io("open temporary COPC file", e))
545 }
546
547 fn commit(mut self) -> Result<()> {
548 let file = self
549 .file
550 .take()
551 .ok_or_else(|| Error::InvalidInput("temporary COPC file already committed".into()))?;
552 file.persist(&self.final_path)
553 .map_err(|e| Error::io("persist COPC file", e.error))?;
554 sync_parent_directory(&self.final_path)?;
555 Ok(())
556 }
557}
558
559#[cfg(unix)]
560fn sync_parent_directory(path: &Path) -> Result<()> {
561 let parent = path
562 .parent()
563 .filter(|parent| !parent.as_os_str().is_empty())
564 .unwrap_or_else(|| Path::new("."));
565 File::open(parent)
566 .and_then(|directory| directory.sync_all())
567 .map_err(|e| Error::io("sync COPC output directory", e))
568}
569
570#[cfg(not(unix))]
571fn sync_parent_directory(_path: &Path) -> Result<()> {
572 Ok(())
573}
574
575#[allow(clippy::too_many_arguments)]
578fn encode_node_points<S: CopcPointSource>(
579 node: &crate::lod::LodNodeRange,
580 index_reader: &mut BufReader<File>,
581 source: &S,
582 fields: &mut CopcPointFields,
583 raw: &mut Vec<u8>,
584 record_len: usize,
585 scale: (f64, f64, f64),
586 offset: (f64, f64, f64),
587 point_format: &LasFormat,
588 cancel: &dyn CancelCheck,
589) -> Result<()> {
590 raw.clear();
591 let raw_len = node
592 .count
593 .checked_mul(record_len)
594 .ok_or_else(|| Error::InvalidInput("node point buffer size overflows usize".into()))?;
595 raw.resize(raw_len, 0);
596 index_reader
597 .seek(SeekFrom::Start(node.start))
598 .map_err(|e| Error::io("seek LOD order", e))?;
599 for point_index in 0..node.count {
600 if point_index.is_multiple_of(CANCEL_POLL_STRIDE) {
601 cancel.check()?;
602 }
603 let source_index = index_reader
604 .read_u32::<LittleEndian>()
605 .map_err(|e| Error::io("read LOD order", e))? as usize;
606 source.fields_into(source_index, fields)?;
607 encode_point_record(
608 &mut raw[point_index * record_len..(point_index + 1) * record_len],
609 fields,
610 scale,
611 offset,
612 source_index,
613 point_format,
614 )?;
615 }
616 Ok(())
617}
618
619fn hierarchy_entry(key: VoxelKey, offset: u64, byte_size: u64, count: usize) -> Result<Entry> {
620 Ok(Entry {
621 key,
622 offset,
623 byte_size: i32::try_from(byte_size)
624 .map_err(|_| Error::InvalidInput("LAZ chunk exceeds COPC i32 byte size".into()))?,
625 point_count: i32::try_from(count)
626 .map_err(|_| Error::InvalidInput("node point count exceeds COPC i32 range".into()))?,
627 })
628}
629
630#[cfg(not(feature = "parallel"))]
634#[allow(clippy::too_many_arguments)]
635fn compress_nodes<W: Write + Seek + Send + Sync, S: CopcPointSource>(
636 writer: &mut W,
637 var_vlr: &laz::LazVlr,
638 lod_index: &crate::lod::LodIndex,
639 source: &S,
640 scale: (f64, f64, f64),
641 offset: (f64, f64, f64),
642 record_len: usize,
643 point_format: &LasFormat,
644 cancel: &dyn CancelCheck,
645) -> Result<Vec<Entry>> {
646 let mut compressor = LasZipCompressor::new(&mut *writer, var_vlr.clone())
647 .map_err(|e| Error::Las(format!("compressor: {e}")))?;
648 let mut hierarchy = Vec::with_capacity(lod_index.nodes.len());
649 let order_path: &Path = lod_index.order_path.as_ref();
650 let mut index_reader = BufReader::with_capacity(
651 INDEX_IO_BUFFER_BYTES,
652 File::open(order_path).map_err(|e| Error::io("open LOD order", e))?,
653 );
654 let mut raw = Vec::new();
655 let mut fields = CopcPointFields::default();
656 let mut chunk_start_file_offset = compressor
657 .get_mut()
658 .stream_position()
659 .map_err(|e| Error::io("record chunk start", e))?;
660 chunk_start_file_offset = chunk_start_file_offset
661 .checked_add(8)
662 .ok_or_else(|| Error::InvalidInput("LAZ point-data offset overflows u64".into()))?;
663
664 for node in &lod_index.nodes {
665 cancel.check()?;
666 encode_node_points(
667 node,
668 &mut index_reader,
669 source,
670 &mut fields,
671 &mut raw,
672 record_len,
673 scale,
674 offset,
675 point_format,
676 cancel,
677 )?;
678 compressor
679 .compress_many(&raw)
680 .map_err(|e| Error::Las(format!("compress chunk: {e}")))?;
681 compressor
682 .finish_current_chunk()
683 .map_err(|e| Error::Las(format!("finish chunk: {e}")))?;
684 let after = compressor
685 .get_mut()
686 .stream_position()
687 .map_err(|e| Error::io("record chunk end", e))?;
688 hierarchy.push(hierarchy_entry(
689 node.key,
690 chunk_start_file_offset,
691 after.checked_sub(chunk_start_file_offset).ok_or_else(|| {
692 Error::InvalidData("LAZ compressor moved before the chunk start".into())
693 })?,
694 node.count,
695 )?);
696 chunk_start_file_offset = after;
697 }
698
699 cancel.check()?;
700 compressor
701 .done()
702 .map_err(|e| Error::Las(format!("finish compressor: {e}")))?;
703 Ok(hierarchy)
704}
705
706#[cfg(feature = "parallel")]
716#[allow(clippy::too_many_arguments)]
717fn compress_nodes<W: Write + Seek + Send, S: CopcPointSource>(
718 writer: &mut W,
719 var_vlr: &laz::LazVlr,
720 lod_index: &crate::lod::LodIndex,
721 source: &S,
722 scale: (f64, f64, f64),
723 offset: (f64, f64, f64),
724 record_len: usize,
725 point_format: &LasFormat,
726 cancel: &dyn CancelCheck,
727) -> Result<Vec<Entry>> {
728 use laz::laszip::{ChunkTable, ChunkTableEntry};
729 use rayon::prelude::*;
730
731 let table_offset_position = writer
732 .stream_position()
733 .map_err(|e| Error::io("record chunk table offset position", e))?;
734 writer
735 .write_i64::<LittleEndian>(-1)
736 .map_err(|e| Error::io("write chunk table offset placeholder", e))?;
737
738 let mut hierarchy = Vec::with_capacity(lod_index.nodes.len());
739 let mut chunk_table = ChunkTable::with_capacity(lod_index.nodes.len());
740 let order_path: &Path = lod_index.order_path.as_ref();
741 let mut index_reader = BufReader::with_capacity(
742 INDEX_IO_BUFFER_BYTES,
743 File::open(order_path).map_err(|e| Error::io("open LOD order", e))?,
744 );
745 let mut fields = CopcPointFields::default();
746 let mut chunk_start_file_offset = table_offset_position + 8;
747 let batch_size = rayon::current_num_threads().max(1) * 2;
748
749 for batch in lod_index.nodes.chunks(batch_size) {
750 cancel.check()?;
751 let mut raw_chunks = Vec::with_capacity(batch.len());
752 for node in batch {
753 let mut raw = Vec::new();
754 encode_node_points(
755 node,
756 &mut index_reader,
757 source,
758 &mut fields,
759 &mut raw,
760 record_len,
761 scale,
762 offset,
763 point_format,
764 cancel,
765 )?;
766 raw_chunks.push(raw);
767 }
768
769 let compressed: Vec<Result<Vec<u8>>> = raw_chunks
770 .par_iter()
771 .map(|raw| compress_standalone_chunk(raw, var_vlr))
772 .collect();
773
774 for (node, chunk) in batch.iter().zip(compressed) {
775 let chunk = chunk?;
776 writer
777 .write_all(&chunk)
778 .map_err(|e| Error::io("write LAZ chunk", e))?;
779 hierarchy.push(hierarchy_entry(
780 node.key,
781 chunk_start_file_offset,
782 chunk.len() as u64,
783 node.count,
784 )?);
785 chunk_table.push(ChunkTableEntry {
786 point_count: node.count as u64,
787 byte_count: chunk.len() as u64,
788 });
789 chunk_start_file_offset = chunk_start_file_offset
790 .checked_add(chunk.len() as u64)
791 .ok_or_else(|| Error::InvalidInput("LAZ point-data offset overflows u64".into()))?;
792 }
793 }
794
795 cancel.check()?;
796 let chunk_table_position = writer
797 .stream_position()
798 .map_err(|e| Error::io("record chunk table position", e))?;
799 chunk_table
800 .write_to(&mut *writer, var_vlr)
801 .map_err(|e| Error::io("write chunk table", e))?;
802 let end_position = writer
803 .stream_position()
804 .map_err(|e| Error::io("record chunk table end", e))?;
805 writer
806 .seek(SeekFrom::Start(table_offset_position))
807 .map_err(|e| Error::io("seek chunk table offset", e))?;
808 let chunk_table_position = i64::try_from(chunk_table_position)
809 .map_err(|_| Error::InvalidInput("LAZ chunk table offset exceeds i64 range".into()))?;
810 writer
811 .write_i64::<LittleEndian>(chunk_table_position)
812 .map_err(|e| Error::io("patch chunk table offset", e))?;
813 writer
814 .seek(SeekFrom::Start(end_position))
815 .map_err(|e| Error::io("seek end of point data", e))?;
816 Ok(hierarchy)
817}
818
819#[cfg(feature = "parallel")]
822fn compress_standalone_chunk(raw_points: &[u8], var_vlr: &laz::LazVlr) -> Result<Vec<u8>> {
823 let mut cursor = std::io::Cursor::new(Vec::new());
824 let mut compressor = LasZipCompressor::new(&mut cursor, var_vlr.clone())
825 .map_err(|e| Error::Las(format!("chunk compressor: {e}")))?;
826 compressor
827 .compress_many(raw_points)
828 .map_err(|e| Error::Las(format!("compress chunk: {e}")))?;
829 compressor
830 .done()
831 .map_err(|e| Error::Las(format!("finish chunk: {e}")))?;
832 drop(compressor);
833 let bytes = cursor.into_inner();
834 let table_position = i64::from_le_bytes(
837 bytes[0..8]
838 .try_into()
839 .map_err(|_| Error::InvalidData("truncated LAZ chunk stream".into()))?,
840 );
841 let table_position = usize::try_from(table_position)
842 .map_err(|_| Error::InvalidData("invalid LAZ chunk table offset".into()))?;
843 if table_position < 8 || table_position > bytes.len() {
844 return Err(Error::InvalidData(
845 "LAZ chunk table offset out of range".into(),
846 ));
847 }
848 Ok(bytes[8..table_position].to_vec())
849}
850
851fn encode_point_record(
855 buf: &mut [u8],
856 fields: &CopcPointFields,
857 scale: (f64, f64, f64),
858 offset: (f64, f64, f64),
859 point_index: usize,
860 format: &LasFormat,
861) -> Result<()> {
862 debug_assert!(format.is_extended && !format.has_nir && !format.has_waveform);
863 debug_assert_eq!(usize::from(format.len()), buf.len());
864 let (ix, iy, iz) = quantize_xyz(point_index, fields.x, fields.y, fields.z, scale, offset)?;
865 buf[0..4].copy_from_slice(&ix.to_le_bytes());
866 buf[4..8].copy_from_slice(&iy.to_le_bytes());
867 buf[8..12].copy_from_slice(&iz.to_le_bytes());
868 buf[12..14].copy_from_slice(&fields.intensity.to_le_bytes());
869 buf[14] = fields.return_number | (fields.number_of_returns << 4);
870 buf[15] = fields.synthetic
871 | (fields.key_point << 1)
872 | (fields.withheld << 2)
873 | (fields.overlap << 3)
874 | (fields.scan_channel << 4)
875 | (fields.scan_direction_flag << 6)
876 | (fields.edge_of_flight_line << 7);
877 buf[16] = fields.classification;
878 buf[17] = fields.user_data;
879 buf[18..20].copy_from_slice(&scan_angle_to_las_scaled(fields.scan_angle).to_le_bytes());
880 buf[20..22].copy_from_slice(&fields.point_source_id.to_le_bytes());
881 buf[22..30].copy_from_slice(&fields.gps_time.to_le_bytes());
882 let mut cursor = 30;
883 if format.has_color {
884 buf[30..32].copy_from_slice(&fields.red.to_le_bytes());
885 buf[32..34].copy_from_slice(&fields.green.to_le_bytes());
886 buf[34..36].copy_from_slice(&fields.blue.to_le_bytes());
887 cursor = 36;
888 }
889 if fields.extra_bytes.len() != buf.len() - cursor {
890 return Err(Error::InvalidInput(format!(
891 "point {point_index} has {} extra byte(s), expected {}",
892 fields.extra_bytes.len(),
893 buf.len() - cursor
894 )));
895 }
896 buf[cursor..].copy_from_slice(&fields.extra_bytes);
897 Ok(())
898}
899
900#[cfg(test)]
901mod tests {
902 use super::*;
903
904 use las::{raw, Color};
905
906 #[test]
909 fn direct_point_encoding_matches_las_raw_point() {
910 let fields = CopcPointFields {
911 x: 12.345,
912 y: -67.89,
913 z: 101.5,
914 intensity: 0xBEEF,
915 return_number: 3,
916 number_of_returns: 5,
917 synthetic: 1,
918 key_point: 0,
919 withheld: 1,
920 overlap: 0,
921 scan_channel: 2,
922 scan_direction_flag: 1,
923 edge_of_flight_line: 0,
924 classification: 6,
925 user_data: 0x42,
926 scan_angle: -30.25,
927 point_source_id: 0xCAFE,
928 gps_time: 1.234e9,
929 red: 1_000,
930 green: 2_000,
931 blue: 3_000,
932 extra_bytes: Vec::new(),
933 };
934 let scale = (0.001, 0.001, 0.001);
935 let offset = (0.0, 0.0, 0.0);
936
937 for (format_id, extra_bytes) in [(6u8, 0u16), (6, 3), (7, 0), (7, 5)] {
938 let mut format = LasFormat::new(format_id).unwrap();
939 format.extra_bytes = extra_bytes;
940 let mut fields = fields.clone();
941 fields.extra_bytes = (0..extra_bytes).map(|byte| byte as u8 ^ 0xA5).collect();
942
943 let mut direct = vec![0u8; usize::from(format.len())];
944 encode_point_record(&mut direct, &fields, scale, offset, 0, &format).unwrap();
945
946 let (ix, iy, iz) =
947 quantize_xyz(0, fields.x, fields.y, fields.z, scale, offset).unwrap();
948 let class_flags = fields.synthetic
949 | (fields.key_point << 1)
950 | (fields.withheld << 2)
951 | (fields.overlap << 3);
952 let reference_point = raw::Point {
953 x: ix,
954 y: iy,
955 z: iz,
956 intensity: fields.intensity,
957 flags: raw::point::Flags::ThreeByte(
958 fields.return_number | (fields.number_of_returns << 4),
959 class_flags
960 | (fields.scan_channel << 4)
961 | (fields.scan_direction_flag << 6)
962 | (fields.edge_of_flight_line << 7),
963 fields.classification,
964 ),
965 scan_angle: raw::point::ScanAngle::Scaled(scan_angle_to_las_scaled(
966 fields.scan_angle,
967 )),
968 user_data: fields.user_data,
969 point_source_id: fields.point_source_id,
970 gps_time: Some(fields.gps_time),
971 color: format.has_color.then_some(Color::new(
972 fields.red,
973 fields.green,
974 fields.blue,
975 )),
976 waveform: None,
977 nir: None,
978 extra_bytes: fields.extra_bytes.clone(),
979 };
980 let mut reference = Vec::with_capacity(usize::from(format.len()));
981 reference_point.write_to(&mut reference, &format).unwrap();
982
983 assert_eq!(
984 reference, direct,
985 "format {format_id} with {extra_bytes} extra byte(s)"
986 );
987 }
988 }
989}