Skip to content

lasrs-cpp

CI Release License: MIT OR Apache-2.0

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.

Why

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).

Read: lasrs-cpp 11.5 s, PDAL 41.2 s (7 threads) and 43.1 s (12 threads), LASzip 65.6 s. Write: lasrs-cpp 9.8 s, LASzip 42.2 s, PDAL 77.9 s

Features

  • 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 Point structs, as columns (x(), intensity(), ...) or as raw record bytes
  • Read from paths or any std::istream, write to paths or any std::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

Examples

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));

Performance

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/.

Tested platforms

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.

Mapping from las-rs

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.

Getting it

Prebuilt bundles (no Rust needed)

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.

From source (needs Rust)

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.

Project practices

  • 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.

Documentation

License

Licensed under either of

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.

Contribution

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.

About

Fast LAS/LAZ/COPC point cloud I/O for C++: the las-rs API with parallel LAZ, 3-6x faster than PDAL and LASzip

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages