las-laz-js 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/NOTICE +1 -1
- package/README.md +56 -14
- package/package.json +2 -2
- package/src/index.js +1 -1
- package/src/laz/ArithmeticEncoder.js +123 -0
- package/src/laz/IntegerCompressor.js +76 -0
- package/src/laz/LazVlr.js +63 -1
- package/src/laz/chunkTable.js +139 -109
- package/src/laz/chunkWorker.js +49 -18
- package/src/laz/compressChunk.js +107 -0
- package/src/laz/decompressChunk.js +40 -10
- package/src/laz/pool.js +32 -18
- package/src/laz/v2/byte.js +23 -7
- package/src/laz/v2/gpstime11.js +165 -25
- package/src/laz/v2/point10.js +91 -22
- package/src/laz/v2/rgb12.js +62 -7
- package/src/laz/v3/layered.js +138 -40
- package/src/laz/v3/point14.js +231 -33
- package/src/write/encodeRecords.js +3 -2
- package/src/write/writeLas.js +259 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 (2026-10-22)
|
|
4
|
+
|
|
5
|
+
Writes LAZ.
|
|
6
|
+
|
|
7
|
+
- `writeLas(points, { compress: true })` and `createLasWriter(sink, { compress: true })` write LAZ:
|
|
8
|
+
point formats 0–3 (point-wise) and 6–8 (layered), with a LASzip VLR, 50,000-point chunks and a
|
|
9
|
+
chunk table, as PDAL (LAZperf) lays it out. Plain JavaScript, translated from laz-rs
|
|
10
|
+
(Apache-2.0), like the decoders. Formats 4, 5, 9, 10 cannot be compressed (`UNSUPPORTED`).
|
|
11
|
+
- `chunkSize`: points per chunk, a whole number 1 to 4294967294 (default 50,000); ignored without
|
|
12
|
+
`compress`.
|
|
13
|
+
- `threads`: chunks compress on the worker pool the LAZ reader uses, written in chunk order, so the
|
|
14
|
+
bytes are the same for any thread count. Automatic by default: from 4 chunks up, with at most 4
|
|
15
|
+
workers, as gains flatten after about 4 (explicit `threads` are honoured); `1` is the calling
|
|
16
|
+
thread. A 10 M-point LAZ (format 3) rewritten: `writeLas` 7166 ms on one thread, 1708 ms
|
|
17
|
+
automatic (4 workers, 4.2×), 1624 ms with 16; `createLasWriter` 7109 ms, 2033–2246 ms, 1657 ms.
|
|
18
|
+
- With workers, up to about 2 tasks of 4 chunks per worker of records are in flight: about 55 MB
|
|
19
|
+
at the automatic 4 workers with 50,000-point format 3 chunks. That holds across awaited
|
|
20
|
+
`writePoints` calls; unawaited calls, or one huge batch, submit at once. An abandoned writer
|
|
21
|
+
keeps its workers until `close()`.
|
|
22
|
+
- A streaming LAZ writer stops at its first error (a chunk, a sink write, `close()`):
|
|
23
|
+
`writePoints` and `close` keep rejecting. Start a new writer. After `close()`, both reject
|
|
24
|
+
(`ABORTED`).
|
|
25
|
+
- The compressed bytes are PDAL's (LAZperf's) with one lossless exception: in formats 6–8 a GPS
|
|
26
|
+
time of −0 following +0 is coded as a change (LAZperf finds the doubles equal).
|
|
27
|
+
- Extra bytes are not written yet (as for LAS); they arrive with 0.7.0.
|
|
28
|
+
- Checked against PDAL 2.10: the committed fixtures' chunks, chunk tables and LASzip VLRs are
|
|
29
|
+
PDAL's bytes; PDAL reads what we write (`pdal info`, `pdal translate`) and finds the same points
|
|
30
|
+
field by field. Real files compress and read back to the same points: a 1.1 GB, 34-million-point
|
|
31
|
+
LAS streams to a 155 MB LAZ (0.14 of its size) in 6.4 s; a 10-million-point LAZ recompresses to
|
|
32
|
+
the same size in 2.7 s. That file, written by PDAL 1.6 in 2019, comes back as LAS 1.2 format 3 with
|
|
33
|
+
the original's point data byte for byte, and opens in CloudCompare as LAS 1.2 format 3 and as
|
|
34
|
+
LAS 1.4 format 7.
|
|
35
|
+
- Docs: README "Writing LAZ"; the demo says LAZ is read and written.
|
|
36
|
+
|
|
3
37
|
## 0.3.1 (2026-10-21)
|
|
4
38
|
|
|
5
39
|
Worker fixes.
|
package/NOTICE
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
las-laz-js
|
|
2
2
|
Copyright (c) 2026 Brent Buffham (https://kirra-design.com). MIT licence (see LICENSE).
|
|
3
3
|
|
|
4
|
-
The LAZ decoder in src/laz/
|
|
4
|
+
The LAZ decoder and encoder in src/laz/ are a JavaScript translation of laz-rs
|
|
5
5
|
(https://github.com/laz-rs/laz-rs), which is itself a Rust port of LASzip
|
|
6
6
|
(https://laszip.org) by rapidlasso. Those files are licensed under the
|
|
7
7
|
Apache License, Version 2.0 (see LICENSE-APACHE), not the MIT licence:
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# las-laz-js
|
|
2
2
|
|
|
3
|
-
A JavaScript reader and writer for ASPRS **LAS** point clouds
|
|
3
|
+
A JavaScript reader and writer for ASPRS **LAS** point clouds and **LAZ**
|
|
4
4
|
(LASzip-compressed LAS), published on npm as `las-laz-js`. Plain ES modules, no dependencies,
|
|
5
5
|
runs in the browser and in Node — and built for files far larger than memory.
|
|
6
6
|
|
|
@@ -8,15 +8,16 @@ runs in the browser and in Node — and built for files far larger than memory.
|
|
|
8
8
|
|
|
9
9
|
- Working: `openLas` reads the header, VLRs / EVLRs and coordinate system of LAS 1.0–1.4;
|
|
10
10
|
`readPoints` and `chunks` decode every point format (0–10) a window at a time, with filters
|
|
11
|
-
and thinning; `colourize` colours points; `writeLas`
|
|
12
|
-
- LAZ: point formats 0–3 and 6–8,
|
|
13
|
-
|
|
11
|
+
and thinning; `colourize` colours points; `writeLas` and `createLasWriter` write LAS 1.0–1.4.
|
|
12
|
+
- LAZ: point formats 0–3 and 6–8, read exactly like LAS — the same calls, the same options,
|
|
13
|
+
decoded chunk by chunk in plain JavaScript (no WebAssembly) — and written with
|
|
14
|
+
`compress: true`, byte for byte what PDAL (LAZperf) writes (bar one lossless exception, see
|
|
15
|
+
Writing LAZ), compressed in parallel.
|
|
14
16
|
- Big files: a 1.1 GB, 34-million-point LAS reads in under a second in Node, and a 10-million-point
|
|
15
17
|
LAZ decodes on every core (a 10-million-point file in about 0.6 s); neither is held in memory
|
|
16
18
|
whole (see LAZ below for the one exception).
|
|
17
19
|
- Also: a ready-made module Worker (`las-laz-js/worker`) and a
|
|
18
20
|
[Three.js demo](https://brentbuffham.github.io/las-laz-js/).
|
|
19
|
-
- Not yet: writing LAZ (0.3.0).
|
|
20
21
|
|
|
21
22
|
## Install
|
|
22
23
|
|
|
@@ -135,6 +136,7 @@ import { writeLas, createLasWriter } from "las-laz-js";
|
|
|
135
136
|
|
|
136
137
|
var bytes = await writeLas(pts, { version: "1.2", pointFormat: 3, crs: { epsg: 28350 } });
|
|
137
138
|
var v14 = await writeLas(pts, { version: "1.4", pointFormat: 7, crs: { wkt: las.crs.wkt } });
|
|
139
|
+
var laz = await writeLas(pts, { compress: true, pointFormat: 3 }); // LAZ
|
|
138
140
|
|
|
139
141
|
var w = await createLasWriter(sink, { version: "1.2", pointFormat: 3, scale, offset });
|
|
140
142
|
// sink: an OPFS writer from openScratchSyncWriter, a Node FileHandle, or any { write(bytes, position) }
|
|
@@ -146,7 +148,44 @@ A version and point format that do not go together (format 6 in LAS 1.2) are ref
|
|
|
146
148
|
silently changed. Scale and offset are chosen from the bounds when not given. `crs.epsg`
|
|
147
149
|
writes a GeoKey record; `crs.wkt` a WKT record. Point formats 6–10 must carry WKT (LAS 1.4), so
|
|
148
150
|
an EPSG code alone is refused for them — the library has no projection database to turn a code
|
|
149
|
-
into WKT.
|
|
151
|
+
into WKT.
|
|
152
|
+
|
|
153
|
+
| Option | Default | Meaning |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `compress` | `false` | write LAZ (LASzip) instead of LAS: point formats 0–3 and 6–8; formats 4, 5, 9, 10 are refused (`UNSUPPORTED`) |
|
|
156
|
+
| `chunkSize` | `50000` | points per LAZ chunk, a whole number from 1 to 4294967294; ignored without `compress` |
|
|
157
|
+
| `threads` | automatic | workers that compress chunks: automatic by default (from 4 chunks up, at most 4 workers), `1` compresses on the calling thread, `n` asks for n workers; only with `compress` |
|
|
158
|
+
|
|
159
|
+
### Writing LAZ
|
|
160
|
+
|
|
161
|
+
`writeLas(pts, { compress: true })` and `createLasWriter(sink, { compress: true, ... })` write the
|
|
162
|
+
same records as the LAS writers, compressed `chunkSize` points at a time (LASzip's 50,000 by
|
|
163
|
+
default) behind a LASzip VLR, with the chunk table at the end of the point data. The layout and
|
|
164
|
+
the compressed bytes are those PDAL (LAZperf) writes: for the same points, version, format, scale
|
|
165
|
+
and offset, the point data is identical to PDAL's — with one exception: in formats 6–8 a GPS time
|
|
166
|
+
of −0 following +0 is coded as a change, so it reads back as −0 (lossless), where LAZperf compares
|
|
167
|
+
the doubles, finds them equal and writes no change. Point formats 0–3 are written point-wise
|
|
168
|
+
(LASzip item version 2), 6–8 layered (item version 3).
|
|
169
|
+
|
|
170
|
+
- **Speed.** Chunks are independent, so they compress on the same pool of workers that decode
|
|
171
|
+
LAZ, and are written strictly in chunk order: the bytes are the same for any `threads`. Gains
|
|
172
|
+
flatten after about four workers, so with `threads` automatic the writers use at most four (one
|
|
173
|
+
fewer than the cores, if that is less): a 10-million-point format 3 file writes in about 1.7 s
|
|
174
|
+
that way on a 24-core machine, 1.6 s with `threads: 16`, about 7 s on one thread. A write of
|
|
175
|
+
under four chunks (200,000 points) stays on one thread, where workers would cost more than they
|
|
176
|
+
save. If workers cannot start, it falls back to one thread.
|
|
177
|
+
- **Memory.** With workers, up to about 2 tasks of 4 chunks per worker are in flight at once —
|
|
178
|
+
about 55 MB of records at the automatic four workers with 50,000-point format 3 chunks (about
|
|
179
|
+
220 MB at `threads: 16`). That bound holds across awaited `writePoints` calls; calls made
|
|
180
|
+
without waiting, or one huge batch, submit all at once. Pass a smaller `threads` or `chunkSize`
|
|
181
|
+
to bound it further. A writer that is abandoned without `close()` keeps its workers in a
|
|
182
|
+
browser until it is closed.
|
|
183
|
+
- **Errors.** A streaming LAZ writer stops for good at its first error — in a chunk, in a sink
|
|
184
|
+
write or in `close()` — and `writePoints` and `close` keep rejecting with it. Start a new
|
|
185
|
+
writer. After a successful `close()`, both reject (`ABORTED`, "the LAZ writer is closed").
|
|
186
|
+
- **Extra bytes** are not written (the writers have never written them): a file read with
|
|
187
|
+
extra bytes is written without them. They arrive with 0.7.0.
|
|
188
|
+
- The streaming writer needs `scale` and `offset` up front, as for LAS.
|
|
150
189
|
|
|
151
190
|
## In a Worker
|
|
152
191
|
|
|
@@ -160,7 +199,7 @@ worker.postMessage({ id: 2, type: "chunks", payload: { fields: ["x", "y", "z"],
|
|
|
160
199
|
The protocol is documented in `src/workers/workerProtocol.js` and can be driven directly with
|
|
161
200
|
`createWorkerHandler(post)`.
|
|
162
201
|
|
|
163
|
-
**Bundlers.** The worker entry and the LAZ decode workers are found through
|
|
202
|
+
**Bundlers.** The worker entry and the LAZ decode and encode workers are found through
|
|
164
203
|
`new Worker(new URL(..., import.meta.url))`, which Vite and webpack 5 follow. With Vite, keep the
|
|
165
204
|
package out of dependency pre-bundling (`optimizeDeps: { exclude: ["las-laz-js"] }`), so the
|
|
166
205
|
worker files stay next to the module that loads them.
|
|
@@ -182,11 +221,12 @@ COPC, EPT and E57 are not read.
|
|
|
182
221
|
## Provenance
|
|
183
222
|
|
|
184
223
|
The LAS reader, writer and big-file handling come from [Kirra](https://kirra-design.com),
|
|
185
|
-
Brent Buffham's blast-design app, rewritten as a standalone library. The LAZ decoder in
|
|
186
|
-
`src/laz/`
|
|
224
|
+
Brent Buffham's blast-design app, rewritten as a standalone library. The LAZ decoder and encoder in
|
|
225
|
+
`src/laz/` are a JavaScript translation of [laz-rs](https://github.com/laz-rs/laz-rs) by Thomas
|
|
187
226
|
Montaigu, itself a Rust port of [LASzip](https://laszip.org) by Martin Isenburg (rapidlasso).
|
|
188
|
-
It is checked against an independent
|
|
189
|
-
[PDAL](https://pdal.io),
|
|
227
|
+
It is checked against an independent implementation: the test fixtures were compressed by
|
|
228
|
+
[PDAL](https://pdal.io), every record must equal PDAL's own decompression, and what this
|
|
229
|
+
library compresses must equal PDAL's bytes and be read by PDAL.
|
|
190
230
|
Formats are written to the ASPRS LAS 1.4 R15 specification; see `docs/LAS_FORMAT_NOTES.md`.
|
|
191
231
|
|
|
192
232
|
## Development
|
|
@@ -200,12 +240,14 @@ node _dev/makeLazFixtures.mjs # remake test/fixtures/laz with PDAL (needs PDAL
|
|
|
200
240
|
```
|
|
201
241
|
|
|
202
242
|
Private sample files go in the gitignored `samples/` folder, or are listed by path in
|
|
203
|
-
`samples/paths.txt`; the sample tests compare each file with its own header,
|
|
204
|
-
LAS twin (same count and bounds) field by field.
|
|
243
|
+
`samples/paths.txt`; the sample tests compare each file with its own header, each LAZ with a
|
|
244
|
+
LAS twin (same count and bounds) field by field, and compress each file to LAZ and read it back.
|
|
245
|
+
Set `LAS_PDAL=<path to pdal>` to also have PDAL read what this library writes
|
|
246
|
+
(`test/laz/pdalWrite.test.js` and the sample tests).
|
|
205
247
|
|
|
206
248
|
## Licence
|
|
207
249
|
|
|
208
|
-
`MIT AND Apache-2.0`. The library is MIT © Brent Buffham (`LICENSE`). The LAZ decoder in
|
|
250
|
+
`MIT AND Apache-2.0`. The library is MIT © Brent Buffham (`LICENSE`). The LAZ decoder and encoder in
|
|
209
251
|
`src/laz/`, translated from laz-rs / LASzip, is under the Apache License 2.0 (`LICENSE-APACHE`,
|
|
210
252
|
`NOTICE`). Both are permissive: use, change and ship it in open or closed software, keeping the
|
|
211
253
|
notices.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "las-laz-js",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Zero-dependency JavaScript reader and writer for ASPRS LAS point clouds, and reader for LAZ (LASzip), for the browser and Node, built for files larger than memory.",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Zero-dependency JavaScript reader and writer for ASPRS LAS point clouds, and reader and writer for LAZ (LASzip), for the browser and Node, built for files larger than memory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.js",
|
package/src/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//==========================================================
|
|
7
7
|
|
|
8
8
|
/** Library version, matches package.json. */
|
|
9
|
-
export var VERSION = "0.
|
|
9
|
+
export var VERSION = "0.4.0";
|
|
10
10
|
|
|
11
11
|
export { LasError, ERROR_CODES } from "./errors.js";
|
|
12
12
|
export { openLas, LasReader } from "./LasReader.js";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// src/laz/ArithmeticEncoder.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// The LASzip arithmetic encoder, the writing half of ArithmeticDecoder:
|
|
4
|
+
// whatever it writes, the decoder reads back exactly. Bytes go into a
|
|
5
|
+
// growable Uint8Array (64 KiB to start, doubled when full); done()
|
|
6
|
+
// flushes the coder and returns a copy of the bytes written. A carry out
|
|
7
|
+
// of base walks back over those bytes, as laz-rs walks its buffer.
|
|
8
|
+
//
|
|
9
|
+
// Ported from laz-rs src/encoders.rs (https://github.com/laz-rs/laz-rs),
|
|
10
|
+
// itself a port of LASzip by rapidlasso. (c) 2007-2014 martin isenburg,
|
|
11
|
+
// rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
|
|
12
|
+
// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
|
|
13
|
+
// NOTICE). Changes: translated from Rust to JavaScript, Brent Buffham 2026;
|
|
14
|
+
// written into a growable byte array instead of laz-rs's ring buffer.
|
|
15
|
+
// base and range are u32 in Numbers (no bit operators: sums and
|
|
16
|
+
// products stay below 2^53 and wrap by subtraction or modulo).
|
|
17
|
+
// The models are the library's (models.js): the encoder and decoder
|
|
18
|
+
// must share them, as LASzip's do.
|
|
19
|
+
//==========================================================
|
|
20
|
+
|
|
21
|
+
var AC_MIN_LENGTH = 0x01000000;
|
|
22
|
+
var FIRST_ALLOCATION = 65536;
|
|
23
|
+
|
|
24
|
+
/** An arithmetic encoder over a growable byte buffer. */
|
|
25
|
+
export function ArithmeticEncoder() {
|
|
26
|
+
this.buf = new Uint8Array(FIRST_ALLOCATION);
|
|
27
|
+
this.n = 0; // bytes written
|
|
28
|
+
this.base = 0;
|
|
29
|
+
this.range = 0xFFFFFFFF;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
ArithmeticEncoder.prototype._put = function (b) {
|
|
33
|
+
if (this.n === this.buf.length) {
|
|
34
|
+
var bigger = new Uint8Array(this.buf.length * 2);
|
|
35
|
+
bigger.set(this.buf);
|
|
36
|
+
this.buf = bigger;
|
|
37
|
+
}
|
|
38
|
+
this.buf[this.n++] = b;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Add one to the bytes already written (base overflowed 2^32). */
|
|
42
|
+
ArithmeticEncoder.prototype._carry = function () {
|
|
43
|
+
var p = this.n - 1;
|
|
44
|
+
while (this.buf[p] === 0xFF) { this.buf[p] = 0; p--; }
|
|
45
|
+
this.buf[p]++;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
ArithmeticEncoder.prototype._add = function (x) {
|
|
49
|
+
var b = this.base + x;
|
|
50
|
+
if (b >= 4294967296) { b -= 4294967296; this._carry(); }
|
|
51
|
+
this.base = b;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
ArithmeticEncoder.prototype._renorm = function () {
|
|
55
|
+
do {
|
|
56
|
+
this._put(Math.floor(this.base / 16777216));
|
|
57
|
+
this.base = (this.base % 16777216) * 256;
|
|
58
|
+
this.range = (this.range * 256) % 4294967296;
|
|
59
|
+
} while (this.range < AC_MIN_LENGTH);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Encode one bit with an adaptive binary model. */
|
|
63
|
+
ArithmeticEncoder.prototype.encodeBit = function (m, sym) {
|
|
64
|
+
var x = m.bit0Prob * Math.floor(this.range / 8192); // BM_LENGTH_SHIFT
|
|
65
|
+
if (sym === 0) { this.range = x; m.bit0Count++; }
|
|
66
|
+
else { this._add(x); this.range -= x; }
|
|
67
|
+
if (this.range < AC_MIN_LENGTH) this._renorm();
|
|
68
|
+
if (--m.bitsUntilUpdate === 0) m.update();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Encode one symbol with an adaptive multi-symbol model. */
|
|
72
|
+
ArithmeticEncoder.prototype.encodeSymbol = function (m, sym) {
|
|
73
|
+
var x;
|
|
74
|
+
if (sym === m.lastSymbol) {
|
|
75
|
+
x = m.distribution[sym] * Math.floor(this.range / 32768); // DM_LENGTH_SHIFT
|
|
76
|
+
this._add(x);
|
|
77
|
+
this.range -= x;
|
|
78
|
+
} else {
|
|
79
|
+
this.range = Math.floor(this.range / 32768);
|
|
80
|
+
x = m.distribution[sym] * this.range;
|
|
81
|
+
this._add(x);
|
|
82
|
+
this.range = m.distribution[sym + 1] * this.range - x;
|
|
83
|
+
}
|
|
84
|
+
if (this.range < AC_MIN_LENGTH) this._renorm();
|
|
85
|
+
m.symbolCount[sym]++;
|
|
86
|
+
if (--m.symbolsUntilUpdate === 0) m.update();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** `bits` raw bits (1-32) of an unsigned Number. */
|
|
90
|
+
ArithmeticEncoder.prototype.writeBits = function (bits, sym) {
|
|
91
|
+
if (bits > 19) { this.writeShort(sym % 65536); sym = Math.floor(sym / 65536); bits -= 16; }
|
|
92
|
+
this.range = Math.floor(this.range / Math.pow(2, bits));
|
|
93
|
+
this._add(sym * this.range);
|
|
94
|
+
if (this.range < AC_MIN_LENGTH) this._renorm();
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** 16 raw bits. */
|
|
98
|
+
ArithmeticEncoder.prototype.writeShort = function (sym) {
|
|
99
|
+
this.range = Math.floor(this.range / 65536);
|
|
100
|
+
this._add(sym * this.range);
|
|
101
|
+
if (this.range < AC_MIN_LENGTH) this._renorm();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** 32 raw bits (low 16 first), from an unsigned Number. */
|
|
105
|
+
ArithmeticEncoder.prototype.writeInt = function (sym) {
|
|
106
|
+
this.writeShort(sym % 65536);
|
|
107
|
+
this.writeShort(Math.floor(sym / 65536));
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Flush the coder, as laz-rs done().
|
|
112
|
+
* @returns {Uint8Array} a copy of the bytes written (the encoder is finished)
|
|
113
|
+
*/
|
|
114
|
+
ArithmeticEncoder.prototype.done = function () {
|
|
115
|
+
var another = true;
|
|
116
|
+
if (this.range > 2 * AC_MIN_LENGTH) { this._add(AC_MIN_LENGTH); this.range = AC_MIN_LENGTH / 2; }
|
|
117
|
+
else { this._add(AC_MIN_LENGTH / 2); this.range = AC_MIN_LENGTH / 512; another = false; }
|
|
118
|
+
this._renorm();
|
|
119
|
+
this._put(0);
|
|
120
|
+
this._put(0);
|
|
121
|
+
if (another) this._put(0);
|
|
122
|
+
return this.buf.slice(0, this.n);
|
|
123
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/laz/IntegerCompressor.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// LASzip's integer compressor, the writing half of IntegerDecompressor:
|
|
4
|
+
// an integer is coded as a correction to a prediction — first k, the
|
|
5
|
+
// bit length of the correction (one model per context), then the
|
|
6
|
+
// correction's bits (modelled up to bitsHigh bits, raw above). The
|
|
7
|
+
// correction wraps into the integer's range (i32 wrap for 32-bit
|
|
8
|
+
// integers), so the decoder's wrap undoes it.
|
|
9
|
+
//
|
|
10
|
+
// Ported from laz-rs src/compressors.rs (https://github.com/laz-rs/laz-rs),
|
|
11
|
+
// itself a port of LASzip by rapidlasso. (c) 2007-2014 martin isenburg,
|
|
12
|
+
// rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
|
|
13
|
+
// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
|
|
14
|
+
// NOTICE). Changes: translated from Rust to JavaScript, Brent Buffham 2026.
|
|
15
|
+
//==========================================================
|
|
16
|
+
|
|
17
|
+
import { ArithmeticModel, ArithmeticBitModel } from "./models.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {number} [bits] - integer width (default 16; 32 wraps as i32)
|
|
21
|
+
* @param {number} [contexts] - number of k models (default 1)
|
|
22
|
+
* @param {number} [bitsHigh] - correction bits that are modelled (default 8)
|
|
23
|
+
* (laz-rs also takes an explicit range; no LAZ item uses one, so it is left out)
|
|
24
|
+
*/
|
|
25
|
+
export function IntegerCompressor(bits, contexts, bitsHigh) {
|
|
26
|
+
bits = bits === undefined ? 16 : bits;
|
|
27
|
+
this.contexts = contexts === undefined ? 1 : contexts;
|
|
28
|
+
this.bitsHigh = bitsHigh === undefined ? 8 : bitsHigh;
|
|
29
|
+
if (bits && bits < 32) {
|
|
30
|
+
this.corrBits = bits;
|
|
31
|
+
this.corrRange = Math.pow(2, bits);
|
|
32
|
+
this.corrMin = -this.corrRange / 2;
|
|
33
|
+
this.corrMax = this.corrMin + this.corrRange - 1;
|
|
34
|
+
} else {
|
|
35
|
+
this.corrBits = 32;
|
|
36
|
+
this.corrRange = 0;
|
|
37
|
+
this.corrMin = -2147483648;
|
|
38
|
+
this.corrMax = 2147483647;
|
|
39
|
+
}
|
|
40
|
+
this.k = 0; // bit length of the last correction, as IntegerDecompressor.k
|
|
41
|
+
this.mBits = [];
|
|
42
|
+
for (var c = 0; c < this.contexts; c++) this.mBits.push(new ArithmeticModel(this.corrBits + 1));
|
|
43
|
+
this.mCorrector0 = new ArithmeticBitModel();
|
|
44
|
+
this.mCorrector = [];
|
|
45
|
+
for (var i = 1; i <= this.corrBits; i++) this.mCorrector.push(new ArithmeticModel(Math.pow(2, i <= this.bitsHigh ? i : this.bitsHigh)));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {import("./ArithmeticEncoder.js").ArithmeticEncoder} enc
|
|
50
|
+
* @param {number} pred - the prediction (i32)
|
|
51
|
+
* @param {number} real - the integer to write
|
|
52
|
+
* @param {number} context
|
|
53
|
+
*/
|
|
54
|
+
IntegerCompressor.prototype.compress = function (enc, pred, real, context) {
|
|
55
|
+
var corr = (real - pred) | 0; // i32 wrapping_sub
|
|
56
|
+
if (corr < this.corrMin) corr += this.corrRange;
|
|
57
|
+
else if (corr > this.corrMax) corr -= this.corrRange;
|
|
58
|
+
var c1 = corr <= 0 ? -corr : corr - 1, k = 0;
|
|
59
|
+
while (c1 >= 1) { c1 = Math.floor(c1 / 2); k++; }
|
|
60
|
+
this.k = k;
|
|
61
|
+
enc.encodeSymbol(this.mBits[context], k);
|
|
62
|
+
if (k !== 0) {
|
|
63
|
+
if (k < 32) {
|
|
64
|
+
var c = corr >= 0 ? corr - 1 : corr + Math.pow(2, k) - 1;
|
|
65
|
+
if (k <= this.bitsHigh) {
|
|
66
|
+
enc.encodeSymbol(this.mCorrector[k - 1], c);
|
|
67
|
+
} else {
|
|
68
|
+
var k1 = k - this.bitsHigh, low = c % Math.pow(2, k1);
|
|
69
|
+
enc.encodeSymbol(this.mCorrector[k - 1], Math.floor(c / Math.pow(2, k1)));
|
|
70
|
+
enc.writeBits(k1, low);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
enc.encodeBit(this.mCorrector0, corr);
|
|
75
|
+
}
|
|
76
|
+
};
|
package/src/laz/LazVlr.js
CHANGED
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
// rapidlasso; (c) 2019-2023 Thomas Montaigu.
|
|
13
13
|
// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
|
|
14
14
|
// NOTICE). Changes: written
|
|
15
|
-
// in JavaScript from that layout
|
|
15
|
+
// in JavaScript from that layout; lazVlrFor (the writing half) added,
|
|
16
|
+
// Brent Buffham 2026.
|
|
16
17
|
//==========================================================
|
|
17
18
|
|
|
18
19
|
import { LasError, ERROR_CODES } from "../errors.js";
|
|
20
|
+
import { POINT_FORMATS } from "../format/PointFormats.js";
|
|
19
21
|
|
|
20
22
|
/** LASzip compressor kinds. */
|
|
21
23
|
export var COMPRESSOR = Object.freeze({ NONE: 0, POINTWISE: 1, POINTWISE_CHUNKED: 2, LAYERED_CHUNKED: 3 });
|
|
@@ -63,3 +65,63 @@ export function findLazVlr(vlrs) {
|
|
|
63
65
|
}
|
|
64
66
|
return null;
|
|
65
67
|
}
|
|
68
|
+
|
|
69
|
+
// The items of each writable point format as [type, size, version]; the
|
|
70
|
+
// extra-bytes item (Byte / Byte14) follows when the record is longer
|
|
71
|
+
var FORMAT_ITEMS = {
|
|
72
|
+
0: [[ITEM.POINT10, 20, 2]],
|
|
73
|
+
1: [[ITEM.POINT10, 20, 2], [ITEM.GPSTIME11, 8, 2]],
|
|
74
|
+
2: [[ITEM.POINT10, 20, 2], [ITEM.RGB12, 6, 2]],
|
|
75
|
+
3: [[ITEM.POINT10, 20, 2], [ITEM.GPSTIME11, 8, 2], [ITEM.RGB12, 6, 2]],
|
|
76
|
+
6: [[ITEM.POINT14, 30, 3]],
|
|
77
|
+
7: [[ITEM.POINT14, 30, 3], [ITEM.RGB14, 6, 3]],
|
|
78
|
+
8: [[ITEM.POINT14, 30, 3], [ITEM.RGBNIR14, 8, 3]]
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The LASzip VLR a writer puts in a file: compressor 2 (formats 0-3) or 3 (6-8),
|
|
83
|
+
* LASzip 3.4 r3, chunked.
|
|
84
|
+
* @param {number} pointFormat - 0-3 or 6-8 (4, 5, 9, 10 carry waveform or 1.3 items that are not written)
|
|
85
|
+
* @param {number} recordLength - the file's point record length
|
|
86
|
+
* @param {number} chunkSize - points per chunk
|
|
87
|
+
* @returns {{vlr: object, bytes: Uint8Array}} vlr has the parseLazVlr shape; bytes is its 34 + 6 * items payload
|
|
88
|
+
*/
|
|
89
|
+
export function lazVlrFor(pointFormat, recordLength, chunkSize) {
|
|
90
|
+
var layered = pointFormat >= 6;
|
|
91
|
+
if (!FORMAT_ITEMS[pointFormat]) {
|
|
92
|
+
throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ compression of point format " + pointFormat + " is not written (formats 0-3 and 6-8 only)");
|
|
93
|
+
}
|
|
94
|
+
var extra = recordLength - POINT_FORMATS[pointFormat].size;
|
|
95
|
+
if (!(extra >= 0 && extra <= 0xFFFF)) {
|
|
96
|
+
throw new LasError(ERROR_CODES.INVALID_ARGUMENT, "record length " + recordLength + " does not fit point format " + pointFormat + " (" + POINT_FORMATS[pointFormat].size + " bytes)");
|
|
97
|
+
}
|
|
98
|
+
var items = FORMAT_ITEMS[pointFormat].map(function (it) {
|
|
99
|
+
return { type: it[0], size: it[1], version: it[2] };
|
|
100
|
+
});
|
|
101
|
+
if (extra > 0) items.push({ type: layered ? ITEM.BYTE14 : ITEM.BYTE, size: extra, version: layered ? 3 : 2 });
|
|
102
|
+
var vlr = {
|
|
103
|
+
compressor: layered ? COMPRESSOR.LAYERED_CHUNKED : COMPRESSOR.POINTWISE_CHUNKED,
|
|
104
|
+
coder: 0,
|
|
105
|
+
version: { major: 3, minor: 4, revision: 3 },
|
|
106
|
+
options: 0,
|
|
107
|
+
chunkSize: chunkSize,
|
|
108
|
+
variableChunks: false,
|
|
109
|
+
items: items
|
|
110
|
+
};
|
|
111
|
+
var bytes = new Uint8Array(34 + 6 * items.length), v = new DataView(bytes.buffer);
|
|
112
|
+
v.setUint16(0, vlr.compressor, true);
|
|
113
|
+
v.setUint16(2, 0, true);
|
|
114
|
+
v.setUint8(4, 3);
|
|
115
|
+
v.setUint8(5, 4);
|
|
116
|
+
v.setUint16(6, 3, true);
|
|
117
|
+
v.setUint32(8, 0, true);
|
|
118
|
+
v.setUint32(12, chunkSize, true);
|
|
119
|
+
for (var at = 16; at < 32; at += 4) v.setUint32(at, 0xFFFFFFFF, true); // no special EVLRs: count -1, offset -1 (two i64)
|
|
120
|
+
v.setUint16(32, items.length, true);
|
|
121
|
+
items.forEach(function (it, i) {
|
|
122
|
+
v.setUint16(34 + i * 6, it.type, true);
|
|
123
|
+
v.setUint16(36 + i * 6, it.size, true);
|
|
124
|
+
v.setUint16(38 + i * 6, it.version, true);
|
|
125
|
+
});
|
|
126
|
+
return { vlr: vlr, bytes: bytes };
|
|
127
|
+
}
|