las-laz-js 0.3.1 → 0.5.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 +71 -0
- package/NOTICE +1 -1
- package/README.md +200 -19
- package/package.json +2 -2
- package/src/LasReader.js +6 -4
- package/src/filter/dedupe.js +14 -5
- package/src/filter/filters.js +31 -10
- package/src/index.js +5 -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/ops/apply.js +144 -0
- package/src/ops/columns.js +209 -0
- package/src/ops/interleave.js +81 -0
- package/src/ops/polygon.js +83 -0
- package/src/ops/select.js +245 -0
- package/src/ops/transform.js +189 -0
- package/src/read/decodeRecords.js +41 -13
- package/src/workers/workerProtocol.js +8 -2
- package/src/write/encodeRecords.js +3 -2
- package/src/write/writeLas.js +259 -6
|
@@ -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
|
+
}
|
package/src/laz/chunkTable.js
CHANGED
|
@@ -1,109 +1,139 @@
|
|
|
1
|
-
// src/laz/chunkTable.js
|
|
2
|
-
//==========================================================
|
|
3
|
-
// Where each compressed chunk lives. A chunked LAZ file starts its
|
|
4
|
-
// point data with an i64 offset to the chunk table (written last, so
|
|
5
|
-
// a streaming writer leaves -1 there and repeats the offset in the
|
|
6
|
-
// file's final 8 bytes). The table is u32 version, u32 chunk count,
|
|
7
|
-
// then each chunk's byte count (and, for variable-size chunks, its
|
|
8
|
-
// point count), arithmetic-coded as integers predicted from the
|
|
9
|
-
// previous chunk. Chunk 0 starts right after the 8-byte offset.
|
|
10
|
-
// A point-wise (unchunked) file is one chunk running to the end.
|
|
11
|
-
//
|
|
12
|
-
// From laz-rs src/laszip/chunk_table.rs (https://github.com/laz-rs/laz-rs),
|
|
13
|
-
// itself from LASzip by rapidlasso. (c) 2007-2015 martin isenburg,
|
|
14
|
-
// rapidlasso; (c) 2019-2023 Thomas Montaigu.
|
|
15
|
-
// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
|
|
16
|
-
// NOTICE). Changes: written
|
|
17
|
-
// in JavaScript over an async byte source
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
* @
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
var
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
var
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
*
|
|
65
|
-
* @
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
1
|
+
// src/laz/chunkTable.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// Where each compressed chunk lives. A chunked LAZ file starts its
|
|
4
|
+
// point data with an i64 offset to the chunk table (written last, so
|
|
5
|
+
// a streaming writer leaves -1 there and repeats the offset in the
|
|
6
|
+
// file's final 8 bytes). The table is u32 version, u32 chunk count,
|
|
7
|
+
// then each chunk's byte count (and, for variable-size chunks, its
|
|
8
|
+
// point count), arithmetic-coded as integers predicted from the
|
|
9
|
+
// previous chunk. Chunk 0 starts right after the 8-byte offset.
|
|
10
|
+
// A point-wise (unchunked) file is one chunk running to the end.
|
|
11
|
+
//
|
|
12
|
+
// From laz-rs src/laszip/chunk_table.rs (https://github.com/laz-rs/laz-rs),
|
|
13
|
+
// itself from LASzip by rapidlasso. (c) 2007-2015 martin isenburg,
|
|
14
|
+
// rapidlasso; (c) 2019-2023 Thomas Montaigu.
|
|
15
|
+
// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
|
|
16
|
+
// NOTICE). Changes: written
|
|
17
|
+
// in JavaScript over an async byte source; the writing half (encodeChunkTable)
|
|
18
|
+
// added, Brent Buffham 2026.
|
|
19
|
+
//==========================================================
|
|
20
|
+
|
|
21
|
+
import { LasError, ERROR_CODES } from "../errors.js";
|
|
22
|
+
import { ArithmeticDecoder } from "./ArithmeticDecoder.js";
|
|
23
|
+
import { IntegerDecompressor } from "./IntegerDecompressor.js";
|
|
24
|
+
import { ArithmeticEncoder } from "./ArithmeticEncoder.js";
|
|
25
|
+
import { IntegerCompressor } from "./IntegerCompressor.js";
|
|
26
|
+
import { COMPRESSOR } from "./LazVlr.js";
|
|
27
|
+
|
|
28
|
+
// Signed i64 at `at` as a Number (an offset; > 2^53 is refused)
|
|
29
|
+
function i64(bytes, at) {
|
|
30
|
+
var v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
31
|
+
var lo = v.getUint32(at, true), hi = v.getInt32(at + 4, true);
|
|
32
|
+
if (hi > 2097151 || hi < -2097152) throw new LasError(ERROR_CODES.BAD_HEADER, "LAZ chunk table offset is out of range", at);
|
|
33
|
+
return hi * 4294967296 + lo;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Decode the chunk table body.
|
|
38
|
+
* @param {Uint8Array} bytes - from the table's start to the end of the file
|
|
39
|
+
* @param {boolean} withPointCounts - variable-size chunks
|
|
40
|
+
* @param {number} [base] - file offset of bytes[0], for error offsets
|
|
41
|
+
* @returns {Array<{pointCount:number, byteCount:number}>}
|
|
42
|
+
*/
|
|
43
|
+
export function decodeChunkTable(bytes, withPointCounts, base) {
|
|
44
|
+
if (bytes.length < 8) throw new LasError(ERROR_CODES.TRUNCATED, "the LAZ chunk table is cut short");
|
|
45
|
+
var v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
46
|
+
var n = v.getUint32(4, true);
|
|
47
|
+
var out = [];
|
|
48
|
+
if (!n) return out;
|
|
49
|
+
var dec = new ArithmeticDecoder(bytes, 8, bytes.length, base);
|
|
50
|
+
dec.readInitBytes();
|
|
51
|
+
var ic = new IntegerDecompressor(32, 2);
|
|
52
|
+
var prevPoints = 0, prevBytes = 0;
|
|
53
|
+
for (var i = 0; i < n; i++) {
|
|
54
|
+
var points = withPointCounts ? ic.decompress(dec, prevPoints, 0) : 0;
|
|
55
|
+
var size = ic.decompress(dec, prevBytes, 1);
|
|
56
|
+
// coded as i32 predictions of u32 counts: the i32 is the prediction, the u32 the count
|
|
57
|
+
prevPoints = points; prevBytes = size;
|
|
58
|
+
out.push({ pointCount: points < 0 ? points + 4294967296 : points, byteCount: size < 0 ? size + 4294967296 : size });
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Encode the chunk table body: the exact inverse of decodeChunkTable.
|
|
65
|
+
* @param {Array<{pointCount:number, byteCount:number}>} chunks
|
|
66
|
+
* @param {boolean} withPointCounts - variable-size chunks
|
|
67
|
+
* @returns {Uint8Array} u32 version 0, u32 chunk count, then the coded counts
|
|
68
|
+
*/
|
|
69
|
+
export function encodeChunkTable(chunks, withPointCounts) {
|
|
70
|
+
var enc = new ArithmeticEncoder();
|
|
71
|
+
var ic = new IntegerCompressor(32, 2);
|
|
72
|
+
var prevPoints = 0, prevBytes = 0;
|
|
73
|
+
for (var i = 0; i < chunks.length; i++) {
|
|
74
|
+
if (withPointCounts) {
|
|
75
|
+
ic.compress(enc, prevPoints, chunks[i].pointCount, 0);
|
|
76
|
+
prevPoints = chunks[i].pointCount;
|
|
77
|
+
}
|
|
78
|
+
ic.compress(enc, prevBytes, chunks[i].byteCount, 1);
|
|
79
|
+
prevBytes = chunks[i].byteCount;
|
|
80
|
+
}
|
|
81
|
+
var body = enc.done();
|
|
82
|
+
var out = new Uint8Array(8 + body.length);
|
|
83
|
+
var v = new DataView(out.buffer);
|
|
84
|
+
v.setUint32(0, 0, true);
|
|
85
|
+
v.setUint32(4, chunks.length, true);
|
|
86
|
+
out.set(body, 8);
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Every chunk of a LAZ file: its first record, point count and byte range.
|
|
92
|
+
* @param {{size:number, read:function(number, number): Promise<Uint8Array>}} src
|
|
93
|
+
* @param {object} header - parseHeader (offsetToPointData, pointCount, evlrStart)
|
|
94
|
+
* @param {object} vlr - parseLazVlr
|
|
95
|
+
* @returns {Promise<Array<{firstRecord:number, count:number, start:number, end:number}>>}
|
|
96
|
+
*/
|
|
97
|
+
export async function readChunks(src, header, vlr) {
|
|
98
|
+
var dataStart = header.offsetToPointData;
|
|
99
|
+
var total = header.pointCount;
|
|
100
|
+
if (vlr.compressor === COMPRESSOR.POINTWISE) {
|
|
101
|
+
var stop = header.evlrStart > dataStart && header.evlrStart <= src.size ? header.evlrStart : src.size;
|
|
102
|
+
return total ? [{ firstRecord: 0, count: total, start: dataStart, end: stop }] : [];
|
|
103
|
+
}
|
|
104
|
+
if (vlr.compressor !== COMPRESSOR.POINTWISE_CHUNKED && vlr.compressor !== COMPRESSOR.LAYERED_CHUNKED) {
|
|
105
|
+
throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ compressor " + vlr.compressor + " is not one LASzip defines (1-3)");
|
|
106
|
+
}
|
|
107
|
+
if (dataStart + 8 > src.size) throw new LasError(ERROR_CODES.TRUNCATED, "the LAZ point data is cut short", src.size);
|
|
108
|
+
var tableAt = i64(await src.read(dataStart, dataStart + 8), 0);
|
|
109
|
+
if (tableAt <= dataStart) {
|
|
110
|
+
tableAt = i64(await src.read(src.size - 8, src.size), 0); // streaming writers repeat it last
|
|
111
|
+
if (tableAt <= dataStart || tableAt >= src.size) {
|
|
112
|
+
throw new LasError(ERROR_CODES.UNSUPPORTED, "this LAZ file has no chunk table (an unfinished write?); it cannot be read in windows");
|
|
113
|
+
}
|
|
114
|
+
} else if (tableAt >= src.size) {
|
|
115
|
+
throw new LasError(ERROR_CODES.TRUNCATED, "the LAZ chunk table should be at byte " + tableAt + " but the file is " + src.size + " bytes (cut short?)", src.size);
|
|
116
|
+
}
|
|
117
|
+
// Read only the table: EVLRs (which can be huge) may follow it. Each coded entry
|
|
118
|
+
// is two integers of at most ~6 bytes each; 24 bytes per chunk is a safe ceiling.
|
|
119
|
+
var head = await src.read(tableAt, Math.min(src.size, tableAt + 8));
|
|
120
|
+
var nChunks = head.length === 8 ? new DataView(head.buffer, head.byteOffset, 8).getUint32(4, true) : 0;
|
|
121
|
+
var table = decodeChunkTable(await src.read(tableAt, Math.min(src.size, tableAt + 8 + nChunks * 24 + 16)), vlr.variableChunks, tableAt);
|
|
122
|
+
var chunks = [], at = dataStart + 8, first = 0;
|
|
123
|
+
for (var i = 0; i < table.length && first < total; i++) {
|
|
124
|
+
var count = vlr.variableChunks ? table[i].pointCount : Math.min(vlr.chunkSize, total - first);
|
|
125
|
+
var end = at + table[i].byteCount;
|
|
126
|
+
if (end > tableAt) throw new LasError(ERROR_CODES.BAD_HEADER, "LAZ chunk " + i + " runs into the chunk table", at);
|
|
127
|
+
chunks.push({ firstRecord: first, count: count, start: at, end: end });
|
|
128
|
+
first += count;
|
|
129
|
+
at = end;
|
|
130
|
+
}
|
|
131
|
+
if (vlr.variableChunks) {
|
|
132
|
+
// counts come from the table: they must add up to the header's, or a corrupt count
|
|
133
|
+
// would decode past the header's points (or size a huge buffer)
|
|
134
|
+
var listed = table.reduce(function (s, e) { return s + e.pointCount; }, 0);
|
|
135
|
+
if (listed !== total) throw new LasError(ERROR_CODES.BAD_HEADER, "the LAZ chunk table lists " + listed + " points but the header says " + total, tableAt);
|
|
136
|
+
}
|
|
137
|
+
if (first < total) throw new LasError(ERROR_CODES.TRUNCATED, "the LAZ chunk table covers " + first + " of " + total + " points", tableAt);
|
|
138
|
+
return chunks;
|
|
139
|
+
}
|
package/src/laz/chunkWorker.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// src/laz/chunkWorker.js
|
|
2
2
|
//==========================================================
|
|
3
|
-
// Worker entry for parallel LAZ
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Worker entry for parallel LAZ work (src/laz/pool.js): decodes whole
|
|
4
|
+
// chunks into raw LAS records, or compresses raw records into whole
|
|
5
|
+
// chunks, and sends them back (transferred). Runs as a browser module
|
|
6
|
+
// Worker or a Node worker_threads Worker.
|
|
7
|
+
// In (decode, no op): { id, vlr, recordLength, packed: ArrayBuffer, chunks: [{ s, e, count }], base }
|
|
7
8
|
// (s / e index into packed; base is packed's file offset)
|
|
9
|
+
// In (compress): { id, op: "compress", vlr, recordLength, records: ArrayBuffer, counts: number[] }
|
|
10
|
+
// (counts: each chunk's points, back to back from the start of records)
|
|
8
11
|
// Out: { ready: true } once listening, then per task
|
|
9
|
-
// { id, records: ArrayBuffer }
|
|
12
|
+
// decode { id, records: ArrayBuffer }, compress { id, chunks: ArrayBuffer, sizes: number[] }
|
|
13
|
+
// (the chunks back to back, and each one's length), or { id, error: { code, message, offset } }
|
|
10
14
|
//
|
|
11
15
|
// Uses the chunk decoder translated from laz-rs (https://github.com/laz-rs/laz-rs),
|
|
12
16
|
// itself a port of LASzip by rapidlasso. (c) 2007-2015 martin isenburg,
|
|
@@ -16,6 +20,7 @@
|
|
|
16
20
|
//==========================================================
|
|
17
21
|
|
|
18
22
|
import { createChunkDecoder } from "./decompressChunk.js";
|
|
23
|
+
import { createChunkCompressor } from "./compressChunk.js";
|
|
19
24
|
|
|
20
25
|
// node:worker_threads is named at run time so browser bundlers leave it alone
|
|
21
26
|
var NODE_WORKER_THREADS = ["node", "worker_threads"].join(":");
|
|
@@ -31,23 +36,49 @@ if (typeof self !== "undefined" && typeof self.postMessage === "function") {
|
|
|
31
36
|
}
|
|
32
37
|
port.post({ ready: true }); // the pool waits for this before sending work
|
|
33
38
|
|
|
34
|
-
|
|
39
|
+
// The decoder and the compressor, each made once per VLR + record length
|
|
40
|
+
function cache(make) {
|
|
41
|
+
var key = null, made = null;
|
|
42
|
+
return function (m) {
|
|
43
|
+
var k = JSON.stringify(m.vlr) + "/" + m.recordLength;
|
|
44
|
+
if (key !== k) { made = make(m.vlr, m.recordLength); key = k; }
|
|
45
|
+
return made;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
var decoder = cache(createChunkDecoder), compressor = cache(createChunkCompressor);
|
|
35
49
|
|
|
36
50
|
function handle(m) {
|
|
37
51
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
var bytes = new Uint8Array(m.packed);
|
|
41
|
-
var total = m.chunks.reduce(function (s, c) { return s + c.count; }, 0);
|
|
42
|
-
var out = new ArrayBuffer(total * m.recordLength);
|
|
43
|
-
var view = new DataView(out), o = 0;
|
|
44
|
-
for (var i = 0; i < m.chunks.length; i++) {
|
|
45
|
-
var c = m.chunks[i];
|
|
46
|
-
cached.decode(bytes, c.s, c.e, c.count, view, o, m.base);
|
|
47
|
-
o += c.count * m.recordLength;
|
|
48
|
-
}
|
|
49
|
-
port.post({ id: m.id, records: out }, [out]);
|
|
52
|
+
if (m.op === "compress") compress(m);
|
|
53
|
+
else decode(m);
|
|
50
54
|
} catch (err) {
|
|
51
55
|
port.post({ id: m.id, error: { code: err && err.code, message: err && err.message ? err.message : String(err), offset: err && err.offset } });
|
|
52
56
|
}
|
|
53
57
|
}
|
|
58
|
+
|
|
59
|
+
function compress(m) {
|
|
60
|
+
var one = compressor(m), records = new Uint8Array(m.records);
|
|
61
|
+
var chunks = [], p = 0, total = 0;
|
|
62
|
+
for (var i = 0; i < m.counts.length; i++) {
|
|
63
|
+
chunks.push(one(records, p, m.counts[i]));
|
|
64
|
+
p += m.counts[i] * m.recordLength;
|
|
65
|
+
total += chunks[i].length;
|
|
66
|
+
}
|
|
67
|
+
var out = new Uint8Array(total), o = 0;
|
|
68
|
+
chunks.forEach(function (c) { out.set(c, o); o += c.length; });
|
|
69
|
+
port.post({ id: m.id, chunks: out.buffer, sizes: chunks.map(function (c) { return c.length; }) }, [out.buffer]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function decode(m) {
|
|
73
|
+
var dec = decoder(m);
|
|
74
|
+
var bytes = new Uint8Array(m.packed);
|
|
75
|
+
var total = m.chunks.reduce(function (s, c) { return s + c.count; }, 0);
|
|
76
|
+
var out = new ArrayBuffer(total * m.recordLength);
|
|
77
|
+
var view = new DataView(out), o = 0;
|
|
78
|
+
for (var i = 0; i < m.chunks.length; i++) {
|
|
79
|
+
var c = m.chunks[i];
|
|
80
|
+
dec(bytes, c.s, c.e, c.count, view, o, m.base);
|
|
81
|
+
o += c.count * m.recordLength;
|
|
82
|
+
}
|
|
83
|
+
port.post({ id: m.id, records: out }, [out]);
|
|
84
|
+
}
|