@voxelkloud/wasm-codecs
v0.5.1
Published
LAZ decoding for voxelkloud, compiled to wasm from laz-rs: standalone laszip chunks for COPC and EPT, whole files for the single-file tier, and the LAS framing needed to find them.
Maintainers
Readme
@voxelkloud/wasm-codecs
LAZ decoding for voxelkloud, compiled to wasm from laz-rs. 148,064 bytes, 51,691 gzipped.
npm install @voxelkloud/wasm-codecsPure Rust, built with the stock wasm32-unknown-unknown target. The rest of
the ecosystem ships laz-perf through Emscripten; this needs no C toolchain.
What it does, and what it leaves alone
A codec and a frame reader. It answers where are the compressed points and
what are the bytes behind them, and stops there. Which of those bytes is an
intensity and which is a classification differs between COPC, EPT and a bare
.laz — that belongs to the format driver, and the arithmetic here does not
change with it. Everything comes back as raw LAS point records,
little-endian, exactly as an uncompressed .las stores them.
Two decode shapes, because the callers genuinely differ.
Indexed — one chunk at a time, which is what COPC and EPT hand you. Build the decoder once per file; parsing the laszip VLR is the expensive part and it does not change between nodes.
import { initLazCodec, readLasHeader, LazChunkDecoder } from "@voxelkloud/wasm-codecs";
await initLazCodec();
// One ranged GET of the front of the file is enough to find everything.
const header = readLasHeader(await range(url, 0, 4096));
if (!header.vlrsComplete) throw new Error("read further and try again");
const decoder = new LazChunkDecoder(header.findVlr("laszip encoded", 22204)!.data);
for (const node of hierarchy) {
const records = decoder.decode(await range(url, node.offset, node.byteSize), node.pointCount);
}Unindexed — a whole file, for the single-file tier and for an EPT node
fetched entire. Takes .las as readily as .laz.
import { decodeLazFile } from "@voxelkloud/wasm-codecs";
const decoded = decodeLazFile(new Uint8Array(await file.arrayBuffer()));
decoded.points; // decoded.pointCount * decoded.pointSize bytesNotes that will bite you otherwise
initLazCodec is process-wide. Everything exported here is a view onto one
wasm instance, so there is no way to run two isolated instances in one JS
realm. Decode in a worker — which the loader does anyway — and isolation is the
worker's. That is also the recovery path if the module ever traps: a wasm panic
poisons its instance for good, and replacing the worker is the only way back.
Every failure this package anticipates is a thrown Error rather than a trap,
including corrupt chunks and truncated files, but a trap is not something a
library can catch on your behalf.
Free what you take. LasHeader, Vlr, DecodedLas and LazChunkDecoder
own wasm memory that no GC of ours reclaims. Call .free(), or hold them with
using.
Selective decoding is a real saving and a sharp edge.
decodeSelective(chunk, count, LazField.Z | LazField.RGB) skips the layers you
did not ask for, on LAS point formats 6 and up. But an unselected field is not
zeroed: its bytes carry the chunk's first point, repeated, because laszip
stores that one raw. Treat them as undefined.
There is a ceiling. One call refuses to decode more than 1 GiB. A point count is a number in an untrusted file, and without the ceiling a corrupt header becomes an allocation failure that explains nothing.
Testing
Two suites, and the split is deliberate.
codec.test.ts runs against fixtures/, generated by
pnpm fixtures — LAS/LAZ twins across point formats 0, 2, 3, 6, 7, 8 and one
with extra bytes, plus a bare variable-size chunk of each in the shape COPC
stores per node. Those come out of the same library that decodes them, so they
prove the wasm boundary, the framing and the chunk path — not laszip
conformance, which would be the same code agreeing with itself.
real-files.test.ts settles conformance against files this repo did not write:
the 200 octree nodes PotreeConverter wrote twice under demo/potree, once as
.las and once as .laz through LASzip's C++ encoder. Every one of them
decodes to the bytes of its uncompressed twin. It also reads a real COPC file's
header and hierarchy EVLR and decodes its root node. Those files are gitignored
and the suite skips itself when they are absent.
Building from source
Needs cargo, rustup target add wasm32-unknown-unknown, and
cargo install wasm-bindgen-cli --version 0.2.127 — the CLI version must match
the wasm-bindgen pin in Cargo.toml exactly.
Unlike @voxelkloud/wasm-core, this crate does use
wasm-bindgen. Its surface moves megabytes of typed arrays and every entry point
can fail on malformed input, which is the case generated glue pays for; the
kernels next door are pure f64 functions over fixed offsets, where it would not.
MIT.
