Fast LAS / LAZ / COPC reading and writing for C++, powered by the Rust crate las-rs and its parallel LAZ codec laz-rs.
Status: wraps the las-rs 0.11 API for reading, writing and COPC, tested on Windows, Linux and macOS (x64 and arm64), with prebuilt releases. As usual for 0.x versions, the API may still change between minor releases.
las-rs decompresses LAZ chunks in parallel on all cores. lasrs-cpp brings that speed to C++ without re-implementing the codec: the Rust crates sit behind a small C ABI and a modern C++20 API.
On a public 952 MB LAZ file with 105.6 million points, lasrs-cpp reads 3.6x faster than PDAL and 5.7x faster than LASzip, and writes 4.3x faster than LASzip and 7.9x faster than PDAL (see Performance).
- The las-rs API in C++, with the same names: if you know las-rs, you know lasrs-cpp
- LAS 1.0 - 1.4, point formats 0 - 10, extra bytes, waveform fields
- LAZ read and write, parallel over chunks
- COPC read: hierarchy access, level-of-detail and bounds queries
- Header, VLR / EVLR, CRS (WKT, GeoTIFF keys)
- Bulk access: read points in batches, as
Pointstructs, as columns (x(),intensity(), ...) or as raw record bytes - Read from paths or any
std::istream, write to paths or anystd::ostream - Header-only C++20 wrapper with RAII and exceptions, plus a plain C API
- Static linking: no Rust runtime and no extra DLLs in your application
Read a file in batches:
#include <lasrs/lasrs.hpp>
#include <iostream>
int main()
{
auto reader = las::Reader::from_path("cloud.laz");
std::cout << reader.header().number_of_points() << " points\n";
auto points = las::PointDataBuilder().for_header(reader.header()).build();
while (reader.fill_points(1'000'000, points) != 0)
{
for (double z : points.z())
{
// ...
}
}
}Write a LAZ file:
las::Builder builder(las::Version(1, 4));
builder.point_format = las::point::Format(6);
auto writer = las::Writer::from_path("out.laz", builder.into_header());
las::Point point;
point.x = 1.0;
point.gps_time = 42.0;
writer.write_point(point);
writer.close();Query a COPC file:
auto reader = las::CopcReader::from_path("cloud.copc.laz");
const las::Bounds area{{637000, 851000, 0}, {638000, 852000, 1000}};
auto points = reader.query(las::LodSelection::Resolution(1.0), las::BoundsSelection::Within(area));AHN4 tile 25GN2_18 (Amsterdam, public domain): 105.6 million points, LAS 1.4 point format 8, 952 MB. Intel Core i7-10750H (6 cores, 12 threads), 32 GB, Windows 11; best of 3 runs, each case in its own process, file in the OS cache. The chart at the top compares the C++ options (lasrs-cpp, PDAL, LASzip) in their multi-threaded configurations; the tables below have every configuration, plus laz-perf (the codec inside PDAL) and laspy for reference.
Read
| Library | Threads | Seconds | Million points/s | Peak memory (MB) |
|---|---|---|---|---|
| lasrs-cpp | 12 | 11.5 | 9.2 | 99 |
| laspy 2.7 + lazrs (Python) | 12 | 12.6 | 8.4 | 139 |
| PDAL 2.10 | 7 (default) | 41.2 | 2.6 | 84 |
| PDAL 2.10 | 12 | 43.1 | 2.5 | 126 |
| laz-perf 3.4 | 1 | 63.6 | 1.7 | 16 |
| LASzip 3.4 | 1 | 65.6 | 1.6 | 1 |
| lasrs-cpp | 1 | 66.0 | 1.6 | 47 |
| PDAL 2.10 | 1 | 67.1 | 1.6 | 33 |
| laspy 2.7 + lazrs (Python) | 1 | 67.6 | 1.6 | 89 |
Write
| Library | Threads | Seconds | Million points/s | Peak memory (MB) |
|---|---|---|---|---|
| lasrs-cpp | 12 | 9.8 | 10.8 | 89 |
| laspy 2.7 + lazrs (Python) | 12 | 10.9 | 9.7 | 84 |
| lasrs-cpp | 1 | 39.3 | 2.7 | 0 |
| laspy 2.7 + lazrs (Python) | 1 | 41.7 | 2.5 | 0 |
| LASzip 3.4 | 1 | 42.2 | 2.5 | 136 |
| laz-perf 3.4 | 1 | 46.8 | 2.3 | 0 |
| PDAL 2.10 | 1 | 77.9 | 1.4 | 10 |
- Single-threaded, all LAZ codecs are about equally fast; lasrs-cpp wins by using all cores.
- Parallel decoding costs memory: about 100 MB instead of 47 MB for lasrs-cpp, because several LAZ chunks are decompressed at once. Reads stream in 1M-point batches, so memory does not grow with the file.
- Peak memory is how much the process grew during the timed operation; write benchmarks get their input points already in memory, which is not counted.
- Every written file was checked to hold exactly the input points. PDAL's writer drops extra bytes by default; its standard fields are identical.
Reproduce with benchmarks/: pixi run bench
downloads this file and writes the tables, the verification and all written
files to test_data/output/bench/.
Every push runs these in CI:
| Platform | Arch | Compiler | What runs |
|---|---|---|---|
| Windows | x64 | MSVC | full test suite (Debug, Release), bundle |
| Ubuntu 24.04 | x64 | GCC 13, Clang 18 | full test suite (Debug, Release), sanitizers, bundle |
| Ubuntu 24.04 | arm64 | GCC 13 | full test suite (Debug, Release), bundle |
| macOS (Apple Silicon) | arm64 | Apple Clang | full test suite (Debug, Release), bundle |
| macOS (Intel) | x64 | Apple Clang | full test suite (Debug, Release), bundle |
The prebuilt Linux bundles (built on Ubuntu 24.04) are then tested on other distributions, on both x64 and arm64:
| Distribution | glibc | Compiler | What runs |
|---|---|---|---|
| Rocky Linux 8 | 2.28 | GCC 13 (gcc-toolset) | full test suite, C API test |
| Ubuntu 20.04 | 2.31 | GCC 9 | C API test |
| Rocky Linux 9 | 2.34 | GCC 11 | full test suite, C API test |
| Ubuntu 22.04 | 2.35 | GCC 11 | full test suite, C API test |
| Debian 12 | 2.36 | GCC 12 | full test suite, C API test |
| Ubuntu 24.04 | 2.39 | GCC 13 | full test suite, C API test |
| Fedora (latest) | latest | latest GCC | full test suite, C API test |
| Arch Linux (x64 only) | latest | latest GCC | full test suite, C API test |
Ubuntu 20.04 ships a compiler without the C++20 library features the C++
headers use (std::chrono calendar types), so only the C API is tested
there; the library itself works on it.
Names and semantics follow las-rs. Where Rust and C++ differ:
| las-rs | lasrs-cpp |
|---|---|
las::Reader::from_path(p) |
las::Reader::from_path(p) |
X::new(...) |
constructor X(...) |
Result<T> |
returns T, throws las::Error |
Option<T> |
std::optional<T> |
&str, &[u8] |
std::string_view, std::span<const uint8_t> |
iterator (pd.x()) |
std::vector |
impl Read + Seek / impl Write + Seek |
std::istream& / std::ostream& |
method on an enum (t.is_standard()) |
free function (is_standard(t)) |
NaiveDate, Uuid |
std::chrono::year_month_day, std::array<uint8_t, 16> |
las-rs cannot write COPC, so neither can lasrs-cpp.
Let CMake download the bundle for your platform from the releases:
set(LASRS_VERSION v0.2.0)
if(WIN32)
set(lasrs_platform windows-x64)
if(CMAKE_MSVC_RUNTIME_LIBRARY MATCHES "^MultiThreaded" AND NOT CMAKE_MSVC_RUNTIME_LIBRARY MATCHES "DLL")
set(lasrs_platform windows-x64-static-crt)
endif()
set(lasrs_archive zip)
else()
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|aarch64)$")
set(lasrs_arch arm64)
else()
set(lasrs_arch x64)
endif()
if(APPLE)
set(lasrs_platform macos-${lasrs_arch})
else()
set(lasrs_platform linux-${lasrs_arch})
endif()
set(lasrs_archive tar.gz)
endif()
if(POLICY CMP0135)
cmake_policy(SET CMP0135 NEW)
endif()
include(FetchContent)
FetchContent_Declare(lasrs
URL https://github.com/bloom256/lasrs-cpp/releases/download/${LASRS_VERSION}/lasrs-cpp-${LASRS_VERSION}-${lasrs_platform}.${lasrs_archive})
FetchContent_MakeAvailable(lasrs)
find_package(lasrs CONFIG REQUIRED PATHS ${lasrs_SOURCE_DIR} NO_DEFAULT_PATH)
target_link_libraries(my_app PRIVATE lasrs::lasrs)Releases after v0.2.0 are immutable. To pin the download yourself, add
URL_HASH SHA256=<hash> to FetchContent_Declare, with the sha256 shown
for your archive on the release page or in SHA256SUMS.txt; each
platform's archive has its own hash.
Or download an archive yourself and point CMake at it with
-DCMAKE_PREFIX_PATH=<extracted archive> and find_package(lasrs REQUIRED):
| Your build | Download |
|---|---|
Windows x64, MSVC, dynamic CRT (/MD, the default) |
lasrs-cpp-vX.Y.Z-windows-x64.zip |
Windows x64, MSVC, static CRT (/MT, /MTd) |
lasrs-cpp-vX.Y.Z-windows-x64-static-crt.zip |
| Linux x64, glibc 2.28 or newer | lasrs-cpp-vX.Y.Z-linux-x64.tar.gz |
| Linux arm64, glibc 2.28 or newer | lasrs-cpp-vX.Y.Z-linux-arm64.tar.gz |
| macOS Apple Silicon | lasrs-cpp-vX.Y.Z-macos-arm64.tar.gz |
| macOS Intel | lasrs-cpp-vX.Y.Z-macos-x64.tar.gz |
Each archive holds include/, the static library in lib/, a CMake
package in lib/cmake/lasrs/ and the licenses in share/lasrs/. Check a
download against SHA256SUMS.txt, or its build provenance with
gh attestation verify <archive> --repo bloom256/lasrs-cpp. Linking
without CMake is described in docs/BUILDING.md.
include(FetchContent)
FetchContent_Declare(lasrs
GIT_REPOSITORY https://github.com/bloom256/lasrs-cpp.git
GIT_TAG v0.2.0)
FetchContent_MakeAvailable(lasrs)
target_link_libraries(my_app PRIVATE lasrs::lasrs)See docs/BUILDING.md for the toolchain.
- Every push to main and every pull request is built and tested by GitHub Actions on Windows, Linux and macOS; the CI badge above shows the current status.
- Formatting (cargo fmt, clang-format), linting (clippy), a check that the committed C header is up to date, license checks (cargo deny) and sanitizers run in CI.
- Dependabot keeps GitHub Actions and Rust dependencies up to date; CodeQL scans the C/C++, Rust and workflow code.
- Security issues: see SECURITY.md; community rules: CODE_OF_CONDUCT.md.
- Semantic versioning and a human-readable CHANGELOG.
- docs/BUILDING.md - toolchain and build instructions
- docs/ARCHITECTURE.md - how the pieces fit together
- docs/ROADMAP.md - milestones and open questions
- CONTRIBUTING.md - how to contribute
- CHANGELOG.md - release notes
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
The static library also contains code from third-party Rust crates (las-rs is MIT, laz-rs is Apache-2.0); their licenses must be preserved when distributing binaries.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.