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.
@@ -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, Brent Buffham 2026.
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
+ }
@@ -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, Brent Buffham 2026.
18
- //==========================================================
19
-
20
- import { LasError, ERROR_CODES } from "../errors.js";
21
- import { ArithmeticDecoder } from "./ArithmeticDecoder.js";
22
- import { IntegerDecompressor } from "./IntegerDecompressor.js";
23
- import { COMPRESSOR } from "./LazVlr.js";
24
-
25
- // Signed i64 at `at` as a Number (an offset; > 2^53 is refused)
26
- function i64(bytes, at) {
27
- var v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
28
- var lo = v.getUint32(at, true), hi = v.getInt32(at + 4, true);
29
- if (hi > 2097151 || hi < -2097152) throw new LasError(ERROR_CODES.BAD_HEADER, "LAZ chunk table offset is out of range", at);
30
- return hi * 4294967296 + lo;
31
- }
32
-
33
- /**
34
- * Decode the chunk table body.
35
- * @param {Uint8Array} bytes - from the table's start to the end of the file
36
- * @param {boolean} withPointCounts - variable-size chunks
37
- * @param {number} [base] - file offset of bytes[0], for error offsets
38
- * @returns {Array<{pointCount:number, byteCount:number}>}
39
- */
40
- export function decodeChunkTable(bytes, withPointCounts, base) {
41
- if (bytes.length < 8) throw new LasError(ERROR_CODES.TRUNCATED, "the LAZ chunk table is cut short");
42
- var v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
43
- var n = v.getUint32(4, true);
44
- var out = [];
45
- if (!n) return out;
46
- var dec = new ArithmeticDecoder(bytes, 8, bytes.length, base);
47
- dec.readInitBytes();
48
- var ic = new IntegerDecompressor(32, 2);
49
- var prevPoints = 0, prevBytes = 0;
50
- for (var i = 0; i < n; i++) {
51
- var points = withPointCounts ? ic.decompress(dec, prevPoints, 0) : 0;
52
- var size = ic.decompress(dec, prevBytes, 1);
53
- // coded as i32 predictions of u32 counts: the i32 is the prediction, the u32 the count
54
- prevPoints = points; prevBytes = size;
55
- out.push({ pointCount: points < 0 ? points + 4294967296 : points, byteCount: size < 0 ? size + 4294967296 : size });
56
- }
57
- return out;
58
- }
59
-
60
- /**
61
- * Every chunk of a LAZ file: its first record, point count and byte range.
62
- * @param {{size:number, read:function(number, number): Promise<Uint8Array>}} src
63
- * @param {object} header - parseHeader (offsetToPointData, pointCount, evlrStart)
64
- * @param {object} vlr - parseLazVlr
65
- * @returns {Promise<Array<{firstRecord:number, count:number, start:number, end:number}>>}
66
- */
67
- export async function readChunks(src, header, vlr) {
68
- var dataStart = header.offsetToPointData;
69
- var total = header.pointCount;
70
- if (vlr.compressor === COMPRESSOR.POINTWISE) {
71
- var stop = header.evlrStart > dataStart && header.evlrStart <= src.size ? header.evlrStart : src.size;
72
- return total ? [{ firstRecord: 0, count: total, start: dataStart, end: stop }] : [];
73
- }
74
- if (vlr.compressor !== COMPRESSOR.POINTWISE_CHUNKED && vlr.compressor !== COMPRESSOR.LAYERED_CHUNKED) {
75
- throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ compressor " + vlr.compressor + " is not one LASzip defines (1-3)");
76
- }
77
- if (dataStart + 8 > src.size) throw new LasError(ERROR_CODES.TRUNCATED, "the LAZ point data is cut short", src.size);
78
- var tableAt = i64(await src.read(dataStart, dataStart + 8), 0);
79
- if (tableAt <= dataStart) {
80
- tableAt = i64(await src.read(src.size - 8, src.size), 0); // streaming writers repeat it last
81
- if (tableAt <= dataStart || tableAt >= src.size) {
82
- throw new LasError(ERROR_CODES.UNSUPPORTED, "this LAZ file has no chunk table (an unfinished write?); it cannot be read in windows");
83
- }
84
- } else if (tableAt >= src.size) {
85
- 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);
86
- }
87
- // Read only the table: EVLRs (which can be huge) may follow it. Each coded entry
88
- // is two integers of at most ~6 bytes each; 24 bytes per chunk is a safe ceiling.
89
- var head = await src.read(tableAt, Math.min(src.size, tableAt + 8));
90
- var nChunks = head.length === 8 ? new DataView(head.buffer, head.byteOffset, 8).getUint32(4, true) : 0;
91
- var table = decodeChunkTable(await src.read(tableAt, Math.min(src.size, tableAt + 8 + nChunks * 24 + 16)), vlr.variableChunks, tableAt);
92
- var chunks = [], at = dataStart + 8, first = 0;
93
- for (var i = 0; i < table.length && first < total; i++) {
94
- var count = vlr.variableChunks ? table[i].pointCount : Math.min(vlr.chunkSize, total - first);
95
- var end = at + table[i].byteCount;
96
- if (end > tableAt) throw new LasError(ERROR_CODES.BAD_HEADER, "LAZ chunk " + i + " runs into the chunk table", at);
97
- chunks.push({ firstRecord: first, count: count, start: at, end: end });
98
- first += count;
99
- at = end;
100
- }
101
- if (vlr.variableChunks) {
102
- // counts come from the table: they must add up to the header's, or a corrupt count
103
- // would decode past the header's points (or size a huge buffer)
104
- var listed = table.reduce(function (s, e) { return s + e.pointCount; }, 0);
105
- if (listed !== total) throw new LasError(ERROR_CODES.BAD_HEADER, "the LAZ chunk table lists " + listed + " points but the header says " + total, tableAt);
106
- }
107
- if (first < total) throw new LasError(ERROR_CODES.TRUNCATED, "the LAZ chunk table covers " + first + " of " + total + " points", tableAt);
108
- return chunks;
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
+ }
@@ -1,12 +1,16 @@
1
1
  // src/laz/chunkWorker.js
2
2
  //==========================================================
3
- // Worker entry for parallel LAZ decoding (src/laz/pool.js): decodes
4
- // whole chunks into raw LAS records and sends them back (transferred).
5
- // Runs as a browser module Worker or a Node worker_threads Worker.
6
- // In: { id, vlr, recordLength, packed: ArrayBuffer, chunks: [{ s, e, count }], base }
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 } or { id, error: { code, message, offset } }
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
- var cached = { key: null, decode: null };
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
- var key = JSON.stringify(m.vlr) + "/" + m.recordLength;
39
- if (cached.key !== key) cached = { key: key, decode: createChunkDecoder(m.vlr, m.recordLength) };
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
+ }