las-laz-js 0.1.0 → 0.2.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.
@@ -14,6 +14,12 @@
14
14
  // so one hot return does not wash the rest out.
15
15
  // classification: conventional viewer colours. ASPRS defines the
16
16
  // codes, not colours; these follow common LiDAR-viewer practice.
17
+ //
18
+ // createChunkColourizer colours a STREAM of chunks with one set of
19
+ // rules, so neighbouring windows match: elevation over the file's
20
+ // header Z range, intensity over the first chunk's percentiles, and
21
+ // the 8-bit decision made once (it only ever moves to 16-bit, when a
22
+ // later chunk holds a value above 255).
17
23
  //==========================================================
18
24
 
19
25
  import { LasError, ERROR_CODES } from "../errors.js";
@@ -81,9 +87,15 @@ function rampColours(values, n, range, ramp) {
81
87
  return out;
82
88
  }
83
89
 
90
+ function maxRgb(points) {
91
+ var n = points.count;
92
+ return Math.max(rangeOf(need(points, "red"), n)[1], rangeOf(need(points, "green"), n)[1], rangeOf(need(points, "blue"), n)[1]);
93
+ }
94
+
84
95
  /**
85
96
  * @param {object} points - PointColumns.
86
- * @param {{by:string, ramp?:string, range?:number[]}} opts
97
+ * @param {{by:string, ramp?:string, range?:number[], eightBit?:boolean}} opts - eightBit
98
+ * overrides the 8-bit colour detection
87
99
  * @returns {Uint8Array} r, g, b per point
88
100
  */
89
101
  export function colourize(points, opts) {
@@ -96,8 +108,7 @@ export function colourize(points, opts) {
96
108
  switch (opts.by) {
97
109
  case "rgb": {
98
110
  var r = need(points, "red"), g = need(points, "green"), b = need(points, "blue");
99
- var max = Math.max(rangeOf(r, n)[1], rangeOf(g, n)[1], rangeOf(b, n)[1]);
100
- var eightBit = max <= 255;
111
+ var eightBit = typeof opts.eightBit === "boolean" ? opts.eightBit : maxRgb(points) <= 255;
101
112
  out = new Uint8Array(n * 3);
102
113
  for (i = 0; i < n; i++) {
103
114
  out[i * 3] = eightBit ? r[i] : Math.min(255, Math.round(r[i] / 256));
@@ -127,3 +138,24 @@ export function colourize(points, opts) {
127
138
  throw new LasError(ERROR_CODES.INVALID_ARGUMENT, "colourize by \"" + opts.by + "\" is not a mode (rgb, intensity, elevation, classification)");
128
139
  }
129
140
  }
141
+
142
+ /**
143
+ * Colour a stream of chunks (reader.chunks) consistently from window to window.
144
+ * @param {{by:string, ramp?:string, range?:number[]}} opts - as colourize
145
+ * @param {{minZ:number, maxZ:number}} [bounds] - the file's header bounds (elevation range)
146
+ * @param {{z?:number}} [localOrigin] - the origin the chunks were read with
147
+ * @returns {function(object): Uint8Array} colours one chunk
148
+ */
149
+ export function createChunkColourizer(opts, bounds, localOrigin) {
150
+ opts = opts || {};
151
+ var range = opts.range || null, eightBit = null;
152
+ if (!range && opts.by === "elevation" && bounds && bounds.maxZ >= bounds.minZ) {
153
+ var oz = localOrigin && isFinite(localOrigin.z) ? localOrigin.z : 0;
154
+ range = [bounds.minZ - oz, bounds.maxZ - oz];
155
+ }
156
+ return function (points) {
157
+ if (opts.by === "intensity" && !range && points.count) range = percentiles(need(points, "intensity"), points.count);
158
+ if (opts.by === "rgb" && points.count && eightBit !== false) eightBit = maxRgb(points) <= 255;
159
+ return colourize(points, Object.assign({}, opts, { range: range || undefined, eightBit: eightBit === null ? undefined : eightBit }));
160
+ };
161
+ }
@@ -20,35 +20,52 @@ export function dedupe(cols, tolerance, opts) {
20
20
  if (!(tolerance > 0)) throw new LasError(ERROR_CODES.INVALID_ARGUMENT, "dedupe tolerance must be > 0");
21
21
  var use3d = !!(opts && opts.z && cols.z);
22
22
  var tol2 = tolerance * tolerance;
23
- var grid = new Map();
24
- var kept = new Uint32Array(cols.count);
25
- var nKept = 0;
23
+ var n = cols.count;
26
24
  var x = cols.x, y = cols.y, z = cols.z;
27
25
  var dzRange = use3d ? 1 : 0;
28
26
 
29
- for (var i = 0; i < cols.count; i++) {
30
- var cx = Math.floor(x[i] / tolerance), cy = Math.floor(y[i] / tolerance);
31
- var cz = use3d ? Math.floor(z[i] / tolerance) : 0;
27
+ // Cell indices, shifted so every neighbour is >= 0
28
+ var lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
29
+ for (var p = 0; p < n; p++) {
30
+ var a0 = Math.floor(x[p] / tolerance), a1 = Math.floor(y[p] / tolerance), a2 = use3d ? Math.floor(z[p] / tolerance) : 0;
31
+ if (a0 < lo[0]) lo[0] = a0; if (a0 > hi[0]) hi[0] = a0;
32
+ if (a1 < lo[1]) lo[1] = a1; if (a1 > hi[1]) hi[1] = a1;
33
+ if (a2 < lo[2]) lo[2] = a2; if (a2 > hi[2]) hi[2] = a2;
34
+ }
35
+ var SY = hi[1] - lo[1] + 3, SZ = hi[2] - lo[2] + 3;
36
+ // A dense numeric key when the cell count fits 2^53; else a hashed one —
37
+ // two cells may then share a key, which only adds candidates: every
38
+ // candidate is still distance-checked, so the result is exact either way
39
+ var dense = (hi[0] - lo[0] + 3) * SY * SZ <= 9007199254740992;
40
+ function key(i, j, k) { return dense ? (i * SY + j) * SZ + k : (i * 73856093 + j * 19349663 + k * 83492791) % 9007199254740881; }
41
+
42
+ // Each cell is a linked list through next[] (Map: key -> most recent point)
43
+ var head = new Map();
44
+ var next = new Int32Array(n);
45
+ var kept = new Uint32Array(n);
46
+ var nKept = 0;
47
+ for (var i = 0; i < n; i++) {
48
+ var cx = Math.floor(x[i] / tolerance) - lo[0] + 1, cy = Math.floor(y[i] / tolerance) - lo[1] + 1;
49
+ var cz = use3d ? Math.floor(z[i] / tolerance) - lo[2] + 1 : 1;
32
50
  var dup = false;
33
51
  for (var dx = -1; dx <= 1 && !dup; dx++) {
34
52
  for (var dy = -1; dy <= 1 && !dup; dy++) {
35
53
  for (var dz = -dzRange; dz <= dzRange && !dup; dz++) {
36
- var cell = grid.get((cx + dx) + "_" + (cy + dy) + "_" + (cz + dz));
37
- if (!cell) continue;
38
- for (var j = 0; j < cell.length; j++) {
39
- var k = cell[j];
54
+ var k = head.get(key(cx + dx, cy + dy, cz + dz));
55
+ while (k !== undefined && k >= 0) {
40
56
  var ex = x[k] - x[i], ey = y[k] - y[i];
41
57
  var ez = use3d ? z[k] - z[i] : 0;
42
58
  if (ex * ex + ey * ey + ez * ez <= tol2) { dup = true; break; }
59
+ k = next[k];
43
60
  }
44
61
  }
45
62
  }
46
63
  }
47
64
  if (dup) continue;
48
- var key = cx + "_" + cy + "_" + cz;
49
- var own = grid.get(key);
50
- if (!own) { own = []; grid.set(key, own); }
51
- own.push(i);
65
+ var own = key(cx, cy, cz);
66
+ var prev = head.get(own);
67
+ next[i] = prev === undefined ? -1 : prev;
68
+ head.set(own, i);
52
69
  kept[nKept++] = i;
53
70
  }
54
71
 
@@ -4,9 +4,10 @@
4
4
  // rejected points cost one or two byte reads (Kirra's classFilter
5
5
  // trick, generalised): classification codes, return numbers and a
6
6
  // 3D box. Filters combine with AND.
7
- // strideFor is Kirra's maxPoints rule: keep every
8
- // floor(count / maxPoints)-th record; 0 keeps every point. The
9
- // reader also caps the kept count at maxPoints.
7
+ // maxPoints thinning is NOT Kirra's floor-stride-and-cap rule, which
8
+ // kept only the start of the file and, run before the filters,
9
+ // could leave nothing: decodeRecords thins the records that PASS,
10
+ // evenly over the whole file (countKept supplies their number).
10
11
  //==========================================================
11
12
 
12
13
  import { POINT_FORMATS } from "../format/PointFormats.js";
@@ -57,12 +58,15 @@ export function makeKeep(filter, header) {
57
58
  }
58
59
 
59
60
  /**
60
- * Record stride for a maxPoints cap (Kirra's rule).
61
- * @param {number} pointCount
62
- * @param {number} maxPoints - 0 = keep every point
63
- * @returns {number} stride, at least 1
61
+ * Count the records in a window of whole records that pass keep.
62
+ * @param {Uint8Array} bytes
63
+ * @param {number} recordLength
64
+ * @param {function(DataView, number): boolean} keep
65
+ * @returns {number}
64
66
  */
65
- export function strideFor(pointCount, maxPoints) {
66
- if (!(maxPoints > 0) || pointCount <= maxPoints) return 1;
67
- return Math.max(1, Math.floor(pointCount / maxPoints));
67
+ export function countKept(bytes, recordLength, keep) {
68
+ var view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
69
+ var n = Math.floor(bytes.length / recordLength), k = 0;
70
+ for (var i = 0; i < n; i++) if (keep(view, i * recordLength)) k++;
71
+ return k;
68
72
  }
@@ -16,7 +16,7 @@ var LE = true;
16
16
  * @param {number} start - offset of the first record in src.
17
17
  * @param {number} count
18
18
  * @param {number} [limit] - records must end at or before this offset (default: end of src).
19
- * @returns {Array<{userId:string, recordId:number, description:string, start:number, bytes:Uint8Array}>}
19
+ * @returns {Array<{userId:string, recordId:number, description:string, start:number, length:number, bytes:Uint8Array}>}
20
20
  */
21
21
  export function parseVlrs(src, start, count, limit) {
22
22
  var end = typeof limit === "number" ? limit : src.length();
@@ -31,6 +31,7 @@ export function parseVlrs(src, start, count, limit) {
31
31
  recordId: src.getUint16(pos + 18, LE),
32
32
  description: readAscii(src, pos + 22, 32),
33
33
  start: pos + 54,
34
+ length: len,
34
35
  bytes: new Uint8Array(src.slice(pos + 54, pos + 54 + len))
35
36
  });
36
37
  pos += 54 + len;
@@ -43,21 +44,23 @@ export function parseVlrs(src, start, count, limit) {
43
44
  * @param {object} src - sync source holding the records (offsets in src).
44
45
  * @param {number} start
45
46
  * @param {number} count
46
- * @returns {Array<{userId:string, recordId:number, description:string, start:number, bytes:Uint8Array}>}
47
+ * @param {boolean} [headersOnly] - src holds only the headers: no bytes (null), no length check
48
+ * @returns {Array<{userId:string, recordId:number, description:string, start:number, length:number, bytes:Uint8Array|null}>}
47
49
  */
48
- export function parseEvlrs(src, start, count) {
50
+ export function parseEvlrs(src, start, count, headersOnly) {
49
51
  var out = [];
50
52
  var pos = start;
51
53
  for (var i = 0; i < count; i++) {
52
54
  if (pos + 60 > src.length()) throw new LasError(ERROR_CODES.BAD_VLR, "EVLR " + (i + 1) + " starts past the end of the file", pos);
53
55
  var len = readU64(src, pos + 20, LE);
54
- if (len === null || pos + 60 + len > src.length()) throw new LasError(ERROR_CODES.BAD_VLR, "EVLR " + (i + 1) + " runs past the end of the file", pos);
56
+ if (len === null || (!headersOnly && pos + 60 + len > src.length())) throw new LasError(ERROR_CODES.BAD_VLR, "EVLR " + (i + 1) + " runs past the end of the file", pos);
55
57
  out.push({
56
58
  userId: readAscii(src, pos + 2, 16),
57
59
  recordId: src.getUint16(pos + 18, LE),
58
60
  description: readAscii(src, pos + 28, 32),
59
61
  start: pos + 60,
60
- bytes: new Uint8Array(src.slice(pos + 60, pos + 60 + len))
62
+ length: len,
63
+ bytes: headersOnly ? null : new Uint8Array(src.slice(pos + 60, pos + 60 + len))
61
64
  });
62
65
  pos += 60 + len;
63
66
  }
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.1.0";
9
+ export var VERSION = "0.2.0";
10
10
 
11
11
  export { LasError, ERROR_CODES } from "./errors.js";
12
12
  export { openLas, LasReader } from "./LasReader.js";
@@ -15,13 +15,13 @@ export { parseVlrs, parseEvlrs } from "./format/LasVlr.js";
15
15
  export { parseCrs } from "./format/LasCrs.js";
16
16
  export { POINT_FORMATS, formatHasRgb } from "./format/PointFormats.js";
17
17
  export { decodeRecords, DEFAULT_FIELDS, FIELD_TYPES } from "./read/decodeRecords.js";
18
- export { makeKeep, strideFor } from "./filter/filters.js";
18
+ export { makeKeep, countKept } from "./filter/filters.js";
19
19
  export { dedupe } from "./filter/dedupe.js";
20
20
  export { MemorySource } from "./io/MemorySource.js";
21
21
  export { BlobSource } from "./io/BlobSource.js";
22
22
  export { NodeFileSource } from "./io/NodeFileSource.js";
23
23
  export { SCRATCH_DIR, openScratchSyncWriter, openScratchReader, removeScratchFile, sweepStaleScratchFiles, listScratchFiles, getScratchFile } from "./io/ScratchBinaryFile.js";
24
- export { colourize, RAMPS, CLASS_COLOURS } from "./colour/colourize.js";
24
+ export { colourize, createChunkColourizer, RAMPS, CLASS_COLOURS } from "./colour/colourize.js";
25
25
  export { writeLas, createLasWriter } from "./write/writeLas.js";
26
26
  export { chooseScaleOffset } from "./write/scaleOffset.js";
27
27
  export { createWorkerHandler } from "./workers/workerProtocol.js";
@@ -26,11 +26,15 @@ export function NodeFileSource(handle, size) {
26
26
  * @param {string} path
27
27
  * @returns {Promise<NodeFileSource>}
28
28
  */
29
+ var NODE_FS = ["node", "fs/promises"].join(":");
30
+
29
31
  NodeFileSource.open = async function (path) {
30
32
  if (typeof path !== "string" || !path) {
31
33
  throw new LasError(ERROR_CODES.INVALID_ARGUMENT, "NodeFileSource needs a file path");
32
34
  }
33
- var fs = await import("node:fs/promises");
35
+ // A non-literal specifier, so browser bundlers (esbuild, webpack, Vite)
36
+ // leave it alone instead of failing to resolve a Node built-in
37
+ var fs = await import(/* webpackIgnore: true */ /* @vite-ignore */ NODE_FS);
34
38
  var handle = await fs.open(path, "r");
35
39
  var stat = await handle.stat();
36
40
  return new NodeFileSource(handle, stat.size);
@@ -49,7 +53,25 @@ NodeFileSource.prototype.read = async function (start, end) {
49
53
  if (end > this.size) {
50
54
  throw new LasError(ERROR_CODES.TRUNCATED, "read past end of " + this.size + "-byte file", end);
51
55
  }
52
- var out = new Uint8Array(end - start);
56
+ return this.readInto(start, end, new Uint8Array(end - start));
57
+ };
58
+
59
+ /**
60
+ * Read bytes [start, end) into the front of target (reused by the reader
61
+ * from window to window, so a big read leaves no per-window garbage).
62
+ * @param {number} start
63
+ * @param {number} end
64
+ * @param {Uint8Array} target - at least end - start bytes
65
+ * @returns {Promise<Uint8Array>} the filled view of target
66
+ */
67
+ NodeFileSource.prototype.readInto = async function (start, end, target) {
68
+ if (start < 0 || end < start) {
69
+ throw new LasError(ERROR_CODES.INVALID_ARGUMENT, "bad read range " + start + ".." + end);
70
+ }
71
+ if (end > this.size) {
72
+ throw new LasError(ERROR_CODES.TRUNCATED, "read past end of " + this.size + "-byte file", end);
73
+ }
74
+ var out = target.subarray(0, end - start);
53
75
  var done = 0;
54
76
  while (done < out.length) {
55
77
  var r = await this._handle.read(out, done, out.length - done, start + done);
@@ -0,0 +1,136 @@
1
+ // src/laz/ArithmeticDecoder.js
2
+ //==========================================================
3
+ // The LASzip arithmetic decoder, over a byte range of one Uint8Array
4
+ // (a LAZ chunk, read whole). Running past the range end is TRUNCATED:
5
+ // a well-formed stream never needs a byte beyond its own end, so a
6
+ // short or corrupt chunk fails loudly instead of decoding garbage.
7
+ //
8
+ // Ported from laz-rs src/decoders.rs (https://github.com/laz-rs/laz-rs),
9
+ // itself a port of LASzip by rapidlasso. (c) 2007-2014 martin isenburg,
10
+ // rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
11
+ // Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
12
+ // NOTICE). Changes: translated from Rust to JavaScript, Brent Buffham 2026;
13
+ // the coder interval laz-rs calls `length` is `range` here.
14
+ // value and range are u32 in Numbers (>>> 0 after every shift); every
15
+ // product of a probability and the shifted range stays below 2^32.
16
+ //==========================================================
17
+
18
+ import { LasError, ERROR_CODES } from "../errors.js";
19
+
20
+ var AC_MAX_LENGTH = 0xFFFFFFFF;
21
+ var AC_MIN_LENGTH = 0x01000000;
22
+
23
+ /**
24
+ * @param {Uint8Array} bytes
25
+ * @param {number} [start] - first byte of the stream (default 0)
26
+ * @param {number} [end] - one past the last byte (default bytes.length)
27
+ * @param {number} [base] - file offset of bytes[0], so errors report file offsets (default 0)
28
+ */
29
+ export function ArithmeticDecoder(bytes, start, end, base) {
30
+ this.bytes = bytes;
31
+ this.base = base || 0;
32
+ this.p = start || 0;
33
+ this.end = end === undefined ? bytes.length : end;
34
+ this.value = 0;
35
+ this.range = AC_MAX_LENGTH;
36
+ }
37
+
38
+ ArithmeticDecoder.prototype._next = function () {
39
+ if (this.p >= this.end) throw new LasError(ERROR_CODES.TRUNCATED, "compressed LAZ data ended early", this.base + this.p);
40
+ return this.bytes[this.p++];
41
+ };
42
+
43
+ /** Read the 4 big-endian bytes that start the stream. */
44
+ ArithmeticDecoder.prototype.readInitBytes = function () {
45
+ this.value = this._next() * 16777216 + this._next() * 65536 + this._next() * 256 + this._next();
46
+ this.range = AC_MAX_LENGTH;
47
+ };
48
+
49
+ ArithmeticDecoder.prototype._renorm = function () {
50
+ do {
51
+ this.value = ((this.value << 8) | this._next()) >>> 0;
52
+ this.range = (this.range << 8) >>> 0;
53
+ } while (this.range < AC_MIN_LENGTH);
54
+ };
55
+
56
+ /** Decode one bit with an adaptive binary model. */
57
+ ArithmeticDecoder.prototype.decodeBit = function (m) {
58
+ var x = m.bit0Prob * (this.range >>> 13); // BM_LENGTH_SHIFT
59
+ var sym;
60
+ if (this.value < x) {
61
+ sym = 0;
62
+ this.range = x;
63
+ m.bit0Count++;
64
+ } else {
65
+ sym = 1;
66
+ this.value -= x;
67
+ this.range -= x;
68
+ }
69
+ if (this.range < AC_MIN_LENGTH) this._renorm();
70
+ if (--m.bitsUntilUpdate === 0) m.update();
71
+ return sym;
72
+ };
73
+
74
+ /** Decode one symbol with an adaptive multi-symbol model. */
75
+ ArithmeticDecoder.prototype.decodeSymbol = function (m) {
76
+ var dist = m.distribution, sym, n, x, k, y = this.range;
77
+ if (m.decoderTable) {
78
+ this.range = this.range >>> 15; // DM_LENGTH_SHIFT
79
+ if (this.range === 0) throw new LasError(ERROR_CODES.TRUNCATED, "corrupt LAZ stream (zero interval)", this.base + this.p);
80
+ var dv = Math.floor(this.value / this.range);
81
+ var t = dv >>> m.tableShift;
82
+ sym = m.decoderTable[t];
83
+ n = m.decoderTable[t + 1] + 1;
84
+ while (n > sym + 1) {
85
+ k = (sym + n) >>> 1;
86
+ if (dist[k] > dv) n = k; else sym = k;
87
+ }
88
+ x = dist[sym] * this.range;
89
+ if (sym !== m.lastSymbol) y = dist[sym + 1] * this.range;
90
+ } else {
91
+ x = 0;
92
+ sym = 0;
93
+ this.range = this.range >>> 15;
94
+ n = m.symbols;
95
+ k = n >>> 1;
96
+ do {
97
+ var z = this.range * dist[k];
98
+ if (z > this.value) { n = k; y = z; } else { sym = k; x = z; }
99
+ k = (sym + n) >>> 1;
100
+ } while (k !== sym);
101
+ }
102
+ this.value -= x;
103
+ this.range = y - x;
104
+ if (this.range < AC_MIN_LENGTH) this._renorm();
105
+ m.symbolCount[sym]++;
106
+ if (--m.symbolsUntilUpdate === 0) m.update();
107
+ return sym;
108
+ };
109
+
110
+ /** `bits` raw bits (1-32), as an unsigned Number. */
111
+ ArithmeticDecoder.prototype.readBits = function (bits) {
112
+ if (bits > 19) {
113
+ var low = this.readShort();
114
+ return this.readBits(bits - 16) * 65536 + low;
115
+ }
116
+ this.range = this.range >>> bits;
117
+ var sym = Math.floor(this.value / this.range);
118
+ this.value -= this.range * sym;
119
+ if (this.range < AC_MIN_LENGTH) this._renorm();
120
+ return sym;
121
+ };
122
+
123
+ /** 16 raw bits. */
124
+ ArithmeticDecoder.prototype.readShort = function () {
125
+ this.range = this.range >>> 16;
126
+ var sym = Math.floor(this.value / this.range);
127
+ this.value -= this.range * sym;
128
+ if (this.range < AC_MIN_LENGTH) this._renorm();
129
+ return sym;
130
+ };
131
+
132
+ /** 32 raw bits (low 16 first), as an unsigned Number. */
133
+ ArithmeticDecoder.prototype.readInt = function () {
134
+ var low = this.readShort();
135
+ return this.readShort() * 65536 + low;
136
+ };
@@ -0,0 +1,77 @@
1
+ // src/laz/IntegerDecompressor.js
2
+ //==========================================================
3
+ // LASzip's integer decompressor: an integer is coded as a correction
4
+ // to a prediction — first k, the bit length of the correction (one
5
+ // model per context), then the correction's bits (modelled up to
6
+ // bitsHigh bits, raw above). The result wraps into the integer's
7
+ // range like the encoder's did (i32 wrap for 32-bit integers).
8
+ //
9
+ // Ported from laz-rs src/decompressors.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
+ //==========================================================
15
+
16
+ import { ArithmeticModel, ArithmeticBitModel } from "./models.js";
17
+
18
+ /**
19
+ * @param {number} [bits] - integer width (default 16; 32 wraps as i32)
20
+ * @param {number} [contexts] - number of k models (default 1)
21
+ * @param {number} [bitsHigh] - correction bits that are modelled (default 8)
22
+ * (laz-rs also takes an explicit range; no LAZ item uses one, so it is left out)
23
+ */
24
+ export function IntegerDecompressor(bits, contexts, bitsHigh) {
25
+ bits = bits === undefined ? 16 : bits;
26
+ this.contexts = contexts === undefined ? 1 : contexts;
27
+ this.bitsHigh = bitsHigh === undefined ? 8 : bitsHigh;
28
+ if (bits && bits < 32) {
29
+ this.corrBits = bits;
30
+ this.corrRange = Math.pow(2, bits);
31
+ this.corrMin = -Math.floor(this.corrRange / 2);
32
+ } else {
33
+ this.corrBits = 32;
34
+ this.corrRange = 0;
35
+ this.corrMin = -2147483648;
36
+ }
37
+ // laz-rs compares and adds `corr_range as i32`
38
+ this.corrRangeI32 = this.corrRange > 2147483647 ? this.corrRange - 4294967296 : this.corrRange;
39
+ this.k = 0;
40
+ this.mBits = [];
41
+ for (var c = 0; c < this.contexts; c++) this.mBits.push(new ArithmeticModel(this.corrBits + 1));
42
+ this.mCorrector0 = new ArithmeticBitModel();
43
+ this.mCorrector = [];
44
+ for (var i = 1; i <= this.corrBits; i++) this.mCorrector.push(new ArithmeticModel(Math.pow(2, i <= this.bitsHigh ? i : this.bitsHigh)));
45
+ }
46
+
47
+ /**
48
+ * @param {import("./ArithmeticDecoder.js").ArithmeticDecoder} dec
49
+ * @param {number} pred - the prediction (i32)
50
+ * @param {number} context
51
+ * @returns {number} the decoded integer (i32; within [0, range) for narrower integers)
52
+ */
53
+ IntegerDecompressor.prototype.decompress = function (dec, pred, context) {
54
+ var k = dec.decodeSymbol(this.mBits[context]);
55
+ var c;
56
+ this.k = k;
57
+ if (k !== 0) {
58
+ if (k < 32) {
59
+ if (k <= this.bitsHigh) {
60
+ c = dec.decodeSymbol(this.mCorrector[k - 1]);
61
+ } else {
62
+ var k1 = k - this.bitsHigh;
63
+ c = dec.decodeSymbol(this.mCorrector[k - 1]) * Math.pow(2, k1) + dec.readBits(k1);
64
+ }
65
+ if (c >= Math.pow(2, k - 1)) c += 1;
66
+ else c -= Math.pow(2, k) - 1;
67
+ } else {
68
+ c = this.corrMin;
69
+ }
70
+ } else {
71
+ c = dec.decodeBit(this.mCorrector0);
72
+ }
73
+ var real = (pred + c) | 0; // i32 wrapping_add
74
+ if (real < 0) real = (real + this.corrRangeI32) | 0;
75
+ else if (real >= this.corrRangeI32) real = (real - this.corrRangeI32) | 0;
76
+ return real;
77
+ };
@@ -0,0 +1,65 @@
1
+ // src/laz/LazVlr.js
2
+ //==========================================================
3
+ // The LASzip VLR ("laszip encoded", record 22204): how the points are
4
+ // compressed — the compressor (point-wise, point-wise chunked, layered
5
+ // chunked), the chunk size (0 / 0xFFFFFFFF = variable, the table then
6
+ // carries each chunk's point count) and the items that make up one
7
+ // record (Point10, GpsTime, RGB12, Byte … or the 1.4 Point14 family),
8
+ // each with its size and coder version.
9
+ //
10
+ // Layout from laz-rs src/laszip/vlr.rs (https://github.com/laz-rs/laz-rs),
11
+ // itself from LASzip by rapidlasso. (c) 2007-2015 martin isenburg,
12
+ // rapidlasso; (c) 2019-2023 Thomas Montaigu.
13
+ // Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
14
+ // NOTICE). Changes: written
15
+ // in JavaScript from that layout, Brent Buffham 2026.
16
+ //==========================================================
17
+
18
+ import { LasError, ERROR_CODES } from "../errors.js";
19
+
20
+ /** LASzip compressor kinds. */
21
+ export var COMPRESSOR = Object.freeze({ NONE: 0, POINTWISE: 1, POINTWISE_CHUNKED: 2, LAYERED_CHUNKED: 3 });
22
+
23
+ /** LASzip item types. */
24
+ export var ITEM = Object.freeze({ BYTE: 0, POINT10: 6, GPSTIME11: 7, RGB12: 8, WAVEPACKET13: 9, POINT14: 10, RGB14: 11, RGBNIR14: 12, WAVEPACKET14: 13, BYTE14: 14 });
25
+
26
+ /** User id and record id of the LASzip VLR. */
27
+ export var LASZIP_USER_ID = "laszip encoded";
28
+ export var LASZIP_RECORD_ID = 22204;
29
+
30
+ var VARIABLE = 0xFFFFFFFF;
31
+
32
+ /**
33
+ * @param {Uint8Array} bytes - the VLR's payload
34
+ * @returns {{compressor:number, coder:number, version:{major:number, minor:number, revision:number},
35
+ * options:number, chunkSize:number, variableChunks:boolean, items:Array<{type:number, size:number, version:number}>}}
36
+ */
37
+ export function parseLazVlr(bytes) {
38
+ if (!bytes || bytes.length < 34) throw new LasError(ERROR_CODES.BAD_VLR, "the LASzip VLR is " + (bytes ? bytes.length : 0) + " bytes (needs 34 or more)");
39
+ var v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
40
+ var n = v.getUint16(32, true);
41
+ if (bytes.length < 34 + n * 6) throw new LasError(ERROR_CODES.BAD_VLR, "the LASzip VLR lists " + n + " items but holds only " + bytes.length + " bytes");
42
+ var chunk = v.getUint32(12, true);
43
+ var items = [];
44
+ for (var i = 0; i < n; i++) items.push({ type: v.getUint16(34 + i * 6, true), size: v.getUint16(36 + i * 6, true), version: v.getUint16(38 + i * 6, true) });
45
+ return {
46
+ compressor: v.getUint16(0, true),
47
+ coder: v.getUint16(2, true),
48
+ version: { major: v.getUint8(4), minor: v.getUint8(5), revision: v.getUint16(6, true) },
49
+ options: v.getUint32(8, true),
50
+ chunkSize: chunk === 0 ? VARIABLE : chunk,
51
+ variableChunks: chunk === 0 || chunk === VARIABLE,
52
+ items: items
53
+ };
54
+ }
55
+
56
+ /**
57
+ * The LASzip VLR among a file's VLRs, or null.
58
+ * @param {Array<{userId:string, recordId:number, bytes:Uint8Array}>} vlrs
59
+ */
60
+ export function findLazVlr(vlrs) {
61
+ for (var i = 0; i < vlrs.length; i++) {
62
+ if (vlrs[i].recordId === LASZIP_RECORD_ID && vlrs[i].userId === LASZIP_USER_ID) return parseLazVlr(vlrs[i].bytes);
63
+ }
64
+ return null;
65
+ }