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/src/laz/pool.js CHANGED
@@ -1,13 +1,14 @@
1
1
  // src/laz/pool.js
2
2
  //==========================================================
3
- // A pool of workers that decode LAZ chunks in parallel (chunks are
4
- // independent, so N cores decode about N times as fast). Browser module
5
- // Workers or Node worker_threads, running src/laz/chunkWorker.js. Tasks
6
- // queue and go to the next free worker; results come back as transferred
7
- // buffers. close() terminates every worker and fails what is still
8
- // queued, so nothing outlives a read. When workers cannot start (no
3
+ // A pool of workers that decode and compress LAZ chunks in parallel
4
+ // (chunks are independent, so N cores do about N times the work).
5
+ // Browser module Workers or Node worker_threads, running
6
+ // src/laz/chunkWorker.js. Tasks queue and go to the next free worker;
7
+ // their buffers go there, and the results come back, transferred.
8
+ // close() terminates every worker and fails what is still queued, so
9
+ // nothing outlives a read or a write. When workers cannot start (no
9
10
  // Worker, a blocked or failing script), createLazPool returns null and
10
- // the reader decodes on its own thread.
11
+ // the reader or writer works on its own thread.
11
12
  //
12
13
  // Uses the chunk decoder translated from laz-rs (https://github.com/laz-rs/laz-rs),
13
14
  // itself a port of LASzip by rapidlasso. (c) 2007-2015 martin isenburg,
@@ -23,7 +24,7 @@ var NODE_OS = ["node", "os"].join(":");
23
24
  var MAX_THREADS = 16;
24
25
 
25
26
  /**
26
- * How many decode workers to use by default: one fewer than the cores, at most 16.
27
+ * How many LAZ workers to use by default: one fewer than the cores, at most 16.
27
28
  * @returns {Promise<number>}
28
29
  */
29
30
  export async function defaultThreads() {
@@ -81,14 +82,14 @@ async function spawn(onMessage, onFail, nodeUrl) {
81
82
  /**
82
83
  * @param {number} n - workers to start (2 or more)
83
84
  * @param {URL} [nodeUrl] - a Node worker entry in place of chunkWorker.js (tests)
84
- * @returns {Promise<{size:number, decode:function(object): Promise<Uint8Array>, close:function(): void}|null>}
85
+ * @returns {Promise<{size:number, decode:function(object): Promise<Uint8Array>, compress:function(object): Promise<{bytes:Uint8Array, sizes:number[]}>, close:function(): void}|null>}
85
86
  */
86
87
  export async function createLazPool(n, nodeUrl) {
87
88
  var workers = [], idle = [], queue = [], waiting = new Map(), nextId = 1, closed = false;
88
89
 
89
90
  function fail(err) {
90
91
  // a worker that dies takes the pool with it: every waiting task fails
91
- var e = err instanceof LasError ? err : new LasError(ERROR_CODES.UNSUPPORTED, "a LAZ decode worker failed: " + (err && err.message ? err.message : String(err)));
92
+ var e = err instanceof LasError ? err : new LasError(ERROR_CODES.UNSUPPORTED, "a LAZ worker failed: " + (err && err.message ? err.message : String(err)));
92
93
  waiting.forEach(function (job) { job.reject(e); });
93
94
  waiting.clear();
94
95
  queue.forEach(function (job) { job.reject(e); });
@@ -101,7 +102,7 @@ export async function createLazPool(n, nodeUrl) {
101
102
  job.worker = w;
102
103
  waiting.set(job.msg.id, job);
103
104
  if (w.hold) w.hold();
104
- w.post(job.msg, [job.msg.packed]);
105
+ w.post(job.msg, job.transfer);
105
106
  }
106
107
  }
107
108
 
@@ -112,7 +113,7 @@ export async function createLazPool(n, nodeUrl) {
112
113
  if (job.worker.release) job.worker.release();
113
114
  idle.push(job.worker);
114
115
  if (m.error) job.reject(new LasError(m.error.code || ERROR_CODES.UNSUPPORTED, m.error.message, m.error.offset));
115
- else job.resolve(new Uint8Array(m.records));
116
+ else job.resolve(job.result(m));
116
117
  pump();
117
118
  }
118
119
 
@@ -127,6 +128,14 @@ export async function createLazPool(n, nodeUrl) {
127
128
  }
128
129
  idle = workers.slice();
129
130
 
131
+ function run(msg, transfer, result) {
132
+ if (closed) return Promise.reject(new LasError(ERROR_CODES.ABORTED, "the LAZ pool is closed"));
133
+ return new Promise(function (resolve, reject) {
134
+ queue.push({ msg: Object.assign({ id: nextId++ }, msg), transfer: transfer, result: result, resolve: resolve, reject: reject });
135
+ pump();
136
+ });
137
+ }
138
+
130
139
  return {
131
140
  size: n,
132
141
  /**
@@ -135,16 +144,21 @@ export async function createLazPool(n, nodeUrl) {
135
144
  * @returns {Promise<Uint8Array>} the chunks' raw LAS records, in order
136
145
  */
137
146
  decode: function (task) {
138
- if (closed) return Promise.reject(new LasError(ERROR_CODES.ABORTED, "the LAZ decode pool is closed"));
139
- return new Promise(function (resolve, reject) {
140
- queue.push({ msg: Object.assign({ id: nextId++ }, task), resolve: resolve, reject: reject });
141
- pump();
142
- });
147
+ return run(task, [task.packed], function (m) { return new Uint8Array(m.records); });
148
+ },
149
+ /**
150
+ * Compress whole chunks from their own copy of the raw records (transferred).
151
+ * @param {{vlr:object, recordLength:number, records:ArrayBuffer, counts:number[]}} task
152
+ * counts: each chunk's points, the chunks back to back from the start of records
153
+ * @returns {Promise<{bytes:Uint8Array, sizes:number[]}>} the chunks, back to back, and each one's length
154
+ */
155
+ compress: function (task) {
156
+ return run(Object.assign({ op: "compress" }, task), [task.records], function (m) { return { bytes: new Uint8Array(m.chunks), sizes: m.sizes }; });
143
157
  },
144
158
  close: function () {
145
159
  if (closed) return;
146
160
  closed = true;
147
- fail(new LasError(ERROR_CODES.ABORTED, "the read stopped"));
161
+ fail(new LasError(ERROR_CODES.ABORTED, "the LAZ pool was closed"));
148
162
  workers.forEach(function (w) { w.close(); });
149
163
  }
150
164
  };
@@ -8,19 +8,23 @@
8
8
  // itself a port of LASzip by rapidlasso. (c) 2007-2014 martin isenburg,
9
9
  // rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
10
10
  // Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
11
- // NOTICE). Changes: translated from Rust to JavaScript, Brent Buffham 2026.
11
+ // NOTICE). Changes: translated from Rust to JavaScript, Brent Buffham 2026;
12
+ // the compressor (ByteCompressor) added beside the decompressor.
12
13
  //==========================================================
13
14
 
14
15
  import { ArithmeticModel } from "../models.js";
15
16
 
16
- /** Byte v2 decompressor for `count` extra bytes: one per chunk. */
17
- export function ByteDecompressor(count) {
18
- this.size = count;
19
- this.last = new Uint8Array(count);
20
- this.models = [];
21
- for (var i = 0; i < count; i++) this.models.push(new ArithmeticModel(256));
17
+ // The state and models both halves start a chunk with: the last bytes and one model per byte
18
+ function initByte(self, count) {
19
+ self.size = count;
20
+ self.last = new Uint8Array(count);
21
+ self.models = [];
22
+ for (var i = 0; i < count; i++) self.models.push(new ArithmeticModel(256));
22
23
  }
23
24
 
25
+ /** Byte v2 decompressor for `count` extra bytes: one per chunk. */
26
+ export function ByteDecompressor(count) { initByte(this, count); }
27
+
24
28
  ByteDecompressor.prototype.first = function (src, s, view, o) {
25
29
  for (var i = 0; i < this.size; i++) { this.last[i] = src[s + i]; view.setUint8(o + i, src[s + i]); }
26
30
  };
@@ -31,3 +35,15 @@ ByteDecompressor.prototype.next = function (dec, view, o) {
31
35
  view.setUint8(o + i, this.last[i]);
32
36
  }
33
37
  };
38
+
39
+ /** Byte v2 compressor for `count` extra bytes: one per chunk, the mirror of ByteDecompressor. */
40
+ export function ByteCompressor(count) { initByte(this, count); }
41
+
42
+ ByteCompressor.prototype.first = function (src, s) { this.last.set(src.subarray(s, s + this.size)); };
43
+
44
+ ByteCompressor.prototype.next = function (enc, src, s) {
45
+ for (var i = 0; i < this.size; i++) {
46
+ enc.encodeSymbol(this.models[i], (src[s + i] - this.last[i]) & 255); // u8 wrapping_sub
47
+ this.last[i] = src[s + i];
48
+ }
49
+ };
@@ -12,11 +12,14 @@
12
12
  // rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
13
13
  // Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
14
14
  // NOTICE). Changes: translated from Rust to JavaScript, i64 as two u32
15
- // halves, Brent Buffham 2026.
15
+ // halves, Brent Buffham 2026; the compressor (GpsTime11Compressor) added
16
+ // beside the decompressor, rounding its multiplier as LAZperf does; its
17
+ // multiplier and 64-bit difference shared with the Point14 compressor.
16
18
  //==========================================================
17
19
 
18
20
  import { ArithmeticModel } from "../models.js";
19
21
  import { IntegerDecompressor } from "../IntegerDecompressor.js";
22
+ import { IntegerCompressor } from "../IntegerCompressor.js";
20
23
 
21
24
  var LE = true;
22
25
  var MULTI = 500, MULTI_MINUS = -10;
@@ -24,18 +27,39 @@ var MULTI_UNCHANGED = MULTI - MULTI_MINUS + 1; // 511
24
27
  var MULTI_CODE_FULL = MULTI - MULTI_MINUS + 2; // 512
25
28
  var MULTI_TOTAL = MULTI - MULTI_MINUS + 6; // 516
26
29
 
30
+ // The state and models both halves start a chunk with; IC is the integer coder
31
+ // (IntegerCompressor or IntegerDecompressor)
32
+ function initGps(self, IC) {
33
+ self.size = 8;
34
+ self.multiModel = new ArithmeticModel(MULTI_TOTAL);
35
+ self.zeroDiffModel = new ArithmeticModel(6);
36
+ self.ic = new IC(32, 9);
37
+ self.last = 0;
38
+ self.nextSeq = 0;
39
+ self.hi = new Int32Array(4);
40
+ self.lo = new Float64Array(4); // u32 values, held exactly
41
+ self.diffs = new Int32Array(4);
42
+ self.extremes = new Int32Array(4);
43
+ }
44
+
45
+ // A full time (hi, lo) starts the next of the four sequences, with no difference yet
46
+ function startSequence(self, hi, lo) {
47
+ self.nextSeq = (self.nextSeq + 1) & 3;
48
+ self.hi[self.nextSeq] = hi;
49
+ self.lo[self.nextSeq] = lo;
50
+ self.last = self.nextSeq;
51
+ self.diffs[self.last] = 0;
52
+ self.extremes[self.last] = 0;
53
+ }
54
+
55
+ // After three extreme differences in a row, the latest becomes the new reference
56
+ function extreme(self, d) {
57
+ if (++self.extremes[self.last] > 3) { self.diffs[self.last] = d; self.extremes[self.last] = 0; }
58
+ }
59
+
27
60
  /** GpsTime11 v2 decompressor: one per chunk. */
28
61
  export function GpsTime11Decompressor() {
29
- this.size = 8;
30
- this.multiModel = new ArithmeticModel(MULTI_TOTAL);
31
- this.zeroDiffModel = new ArithmeticModel(6);
32
- this.ic = new IntegerDecompressor(32, 9);
33
- this.last = 0;
34
- this.nextSeq = 0;
35
- this.hi = new Int32Array(4);
36
- this.lo = new Float64Array(4); // u32 values, held exactly
37
- this.diffs = new Int32Array(4);
38
- this.extremes = new Int32Array(4);
62
+ initGps(this, IntegerDecompressor);
39
63
  }
40
64
 
41
65
  GpsTime11Decompressor.prototype._add = function (i, d) {
@@ -46,17 +70,8 @@ GpsTime11Decompressor.prototype._add = function (i, d) {
46
70
  };
47
71
 
48
72
  GpsTime11Decompressor.prototype._full = function (dec) {
49
- this.nextSeq = (this.nextSeq + 1) & 3;
50
- this.hi[this.nextSeq] = this.ic.decompress(dec, this.hi[this.last], 8);
51
- this.lo[this.nextSeq] = dec.readInt();
52
- this.last = this.nextSeq;
53
- this.diffs[this.last] = 0;
54
- this.extremes[this.last] = 0;
55
- };
56
-
57
- // After three extreme differences in a row, the latest becomes the new reference
58
- GpsTime11Decompressor.prototype._extreme = function (d) {
59
- if (++this.extremes[this.last] > 3) { this.diffs[this.last] = d; this.extremes[this.last] = 0; }
73
+ var hi = this.ic.decompress(dec, this.hi[this.last], 8);
74
+ startSequence(this, hi, dec.readInt());
60
75
  };
61
76
 
62
77
  GpsTime11Decompressor.prototype.first = function (src, s, view, o) {
@@ -88,19 +103,19 @@ GpsTime11Decompressor.prototype.next = function (dec, view, o) {
88
103
  } else if (multi < MULTI_UNCHANGED) {
89
104
  if (multi === 0) {
90
105
  d = this.ic.decompress(dec, 0, 7);
91
- this._extreme(d);
106
+ extreme(this, d);
92
107
  } else if (multi < MULTI) {
93
108
  d = this.ic.decompress(dec, Math.imul(multi, this.diffs[L]), multi < 10 ? 2 : 3);
94
109
  } else if (multi === MULTI) {
95
110
  d = this.ic.decompress(dec, Math.imul(MULTI, this.diffs[L]), 4);
96
- this._extreme(d);
111
+ extreme(this, d);
97
112
  } else {
98
113
  multi = MULTI - multi;
99
114
  if (multi > MULTI_MINUS) {
100
115
  d = this.ic.decompress(dec, Math.imul(multi, this.diffs[L]), 5);
101
116
  } else {
102
117
  d = this.ic.decompress(dec, Math.imul(MULTI_MINUS, this.diffs[L]), 6);
103
- this._extreme(d);
118
+ extreme(this, d);
104
119
  }
105
120
  }
106
121
  this._add(L, d);
@@ -116,3 +131,128 @@ GpsTime11Decompressor.prototype.next = function (dec, view, o) {
116
131
  view.setUint32(o, this.lo[this.last], LE);
117
132
  view.setInt32(o + 4, this.hi[this.last], LE);
118
133
  };
134
+
135
+ // The multiplier between this and the last difference, as LAZperf rounds it:
136
+ // std::round of the f32 quotient (half away from zero), then (int). LASzip and
137
+ // laz-rs add 0.5f in f32 instead, which differs only within an f32 step of a
138
+ // half; the fixtures' oracle is LAZperf. The one quotient past i32 (2^31, a last
139
+ // difference of +-1) converts as x86 does, to -2^31. Either choice decodes the same.
140
+ export function quantizeMulti(curr, last) {
141
+ var q = Math.fround(Math.fround(curr) / Math.fround(last));
142
+ var m = q >= 0 ? Math.floor(q + 0.5) : -Math.floor(0.5 - q);
143
+ return m > 2147483647 ? -2147483648 : m;
144
+ }
145
+
146
+ // A DataView over the caller's records, made once per buffer
147
+ function viewOf(item, src) {
148
+ if (item._src !== src) { item._src = src; item._view = new DataView(src.buffer, src.byteOffset, src.byteLength); }
149
+ return item._view;
150
+ }
151
+
152
+ /** GpsTime11 v2 compressor: one per chunk, the mirror of GpsTime11Decompressor. */
153
+ export function GpsTime11Compressor() {
154
+ initGps(this, IntegerCompressor);
155
+ this._src = null;
156
+ this._view = null;
157
+ }
158
+
159
+
160
+ // (hi, lo) - (hi0, lo0) as an i32, or null when the 64-bit difference does not fit
161
+ // (hi signed, lo unsigned; shared with the Point14 compressor's GPS time)
162
+ export function diff32(hi, lo, hi0, lo0) {
163
+ var dl = lo - lo0, dh = hi - hi0;
164
+ if (dl < 0) { dl += 4294967296; dh -= 1; }
165
+ dh = dh | 0; // the high word of the i64 wrapping_sub
166
+ if (dh === 0 && dl < 2147483648) return dl;
167
+ if (dh === -1 && dl >= 2147483648) return dl - 4294967296;
168
+ return null;
169
+ }
170
+
171
+ // (hi, lo) minus sequence i as an i32, or null when the 64-bit difference does not fit
172
+ GpsTime11Compressor.prototype._diff = function (i, hi, lo) { return diff32(hi, lo, this.hi[i], this.lo[i]); };
173
+
174
+ // The first other sequence (1-3 on from the last) this time is within i32 of, or 0
175
+ GpsTime11Compressor.prototype._other = function (hi, lo) {
176
+ for (var i = 1; i < 4; i++) if (this._diff((this.last + i) & 3, hi, lo) !== null) return i;
177
+ return 0;
178
+ };
179
+
180
+ GpsTime11Compressor.prototype._full = function (enc, hi, lo) {
181
+ this.ic.compress(enc, this.hi[this.last], hi, 8);
182
+ enc.writeInt(lo);
183
+ startSequence(this, hi, lo);
184
+ };
185
+
186
+ GpsTime11Compressor.prototype.first = function (src, s) {
187
+ var v = viewOf(this, src);
188
+ this.lo[0] = v.getUint32(s, LE);
189
+ this.hi[0] = v.getInt32(s + 4, LE);
190
+ };
191
+
192
+ GpsTime11Compressor.prototype.next = function (enc, src, s) {
193
+ var v = viewOf(this, src);
194
+ var lo = v.getUint32(s, LE), hi = v.getInt32(s + 4, LE);
195
+ for (;;) {
196
+ var L = this.last, d, j, multi;
197
+ if (hi === this.hi[L] && lo === this.lo[L]) {
198
+ if (this.diffs[L] === 0) enc.encodeSymbol(this.zeroDiffModel, 0);
199
+ else enc.encodeSymbol(this.multiModel, MULTI_UNCHANGED);
200
+ return;
201
+ }
202
+ d = this._diff(L, hi, lo);
203
+ if (this.diffs[L] === 0) {
204
+ if (d !== null) {
205
+ enc.encodeSymbol(this.zeroDiffModel, 1);
206
+ this.ic.compress(enc, 0, d, 0);
207
+ this.diffs[L] = d;
208
+ this.extremes[L] = 0;
209
+ } else if ((j = this._other(hi, lo))) {
210
+ enc.encodeSymbol(this.zeroDiffModel, j + 2);
211
+ this.last = (L + j) & 3;
212
+ continue; // laz-rs recurses: code again against the new sequence
213
+ } else {
214
+ enc.encodeSymbol(this.zeroDiffModel, 2);
215
+ this._full(enc, hi, lo);
216
+ }
217
+ } else if (d !== null) {
218
+ multi = quantizeMulti(d, this.diffs[L]);
219
+ if (multi === 1) {
220
+ enc.encodeSymbol(this.multiModel, 1);
221
+ this.ic.compress(enc, this.diffs[L], d, 1);
222
+ this.extremes[L] = 0;
223
+ } else if (multi > 0) {
224
+ if (multi < MULTI) {
225
+ enc.encodeSymbol(this.multiModel, multi);
226
+ this.ic.compress(enc, Math.imul(multi, this.diffs[L]), d, multi < 10 ? 2 : 3);
227
+ } else {
228
+ enc.encodeSymbol(this.multiModel, MULTI);
229
+ this.ic.compress(enc, Math.imul(MULTI, this.diffs[L]), d, 4);
230
+ extreme(this, d);
231
+ }
232
+ } else if (multi < 0) {
233
+ if (multi > MULTI_MINUS) {
234
+ enc.encodeSymbol(this.multiModel, MULTI - multi);
235
+ this.ic.compress(enc, Math.imul(multi, this.diffs[L]), d, 5);
236
+ } else {
237
+ enc.encodeSymbol(this.multiModel, MULTI - MULTI_MINUS);
238
+ this.ic.compress(enc, Math.imul(MULTI_MINUS, this.diffs[L]), d, 6);
239
+ extreme(this, d);
240
+ }
241
+ } else {
242
+ enc.encodeSymbol(this.multiModel, 0);
243
+ this.ic.compress(enc, 0, d, 7);
244
+ extreme(this, d);
245
+ }
246
+ } else if ((j = this._other(hi, lo))) {
247
+ enc.encodeSymbol(this.multiModel, MULTI_CODE_FULL + j);
248
+ this.last = (L + j) & 3;
249
+ continue;
250
+ } else {
251
+ enc.encodeSymbol(this.multiModel, MULTI_CODE_FULL);
252
+ this._full(enc, hi, lo);
253
+ }
254
+ this.hi[this.last] = hi;
255
+ this.lo[this.last] = lo;
256
+ return;
257
+ }
258
+ };
@@ -4,8 +4,9 @@
4
4
  // (formats 0-5) starts with. Each point codes which attributes changed
5
5
  // since the last one; X and Y are corrections to a streaming median
6
6
  // of the recent differences, Z to the last height at the same return
7
- // level, all through integer decompressors whose contexts follow the
8
- // return structure.
7
+ // level, all through integer coders whose contexts follow the return
8
+ // structure. The compressor and decompressor below make the same model
9
+ // choices from the same state, so they stay in step point for point.
9
10
  //
10
11
  // Ported from laz-rs src/las/point0.rs, module v2, and the median and
11
12
  // return tables of src/las/utils.rs (https://github.com/laz-rs/laz-rs),
@@ -13,11 +14,13 @@
13
14
  // rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
14
15
  // Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
15
16
  // NOTICE). Changes: translated from Rust to JavaScript, working on raw
16
- // record bytes, Brent Buffham 2026.
17
+ // record bytes, Brent Buffham 2026; the compressor (Point10Compressor)
18
+ // added beside the decompressor, sharing its models, median and tables.
17
19
  //==========================================================
18
20
 
19
21
  import { ArithmeticModel } from "../models.js";
20
22
  import { IntegerDecompressor } from "../IntegerDecompressor.js";
23
+ import { IntegerCompressor } from "../IntegerCompressor.js";
21
24
 
22
25
  var LE = true;
23
26
 
@@ -66,23 +69,34 @@ function models(n, symbols) { var out = []; for (var i = 0; i < n; i++) out.push
66
69
  function lazyModel(table, i) { return table[i] || (table[i] = new ArithmeticModel(256)); }
67
70
  function medians() { var out = []; for (var i = 0; i < 16; i++) out.push(new StreamingMedian()); return out; }
68
71
 
72
+ // The Y and Z coder contexts, from the bit lengths of the X (and Y) corrections just coded
73
+ // (Point14 uses them too)
74
+ export function yContext(single, icDx) { var k = icDx.k; return single + (k < 20 ? k & 0xFFFFFFFE : 20); }
75
+ export function zContext(single, icDx, icDy) { var k = Math.floor((icDx.k + icDy.k) / 2); return single + (k < 18 ? k & 0xFFFFFFFE : 18); }
76
+
77
+ // The state and models both halves start a chunk with; IC is the integer coder
78
+ // (IntegerCompressor or IntegerDecompressor), so each side gets the same widths and contexts
79
+ function initPoint10(self, IC) {
80
+ self.size = 20;
81
+ self.lastIntensity = new Uint16Array(16);
82
+ self.xMedian = medians();
83
+ self.yMedian = medians();
84
+ self.lastHeight = new Int32Array(8);
85
+ self.changedValues = new ArithmeticModel(64);
86
+ self.scanAngleRank = models(2, 256);
87
+ self.bitByte = new Array(256);
88
+ self.classification = new Array(256);
89
+ self.userData = new Array(256);
90
+ self.icIntensity = new IC(16, 4);
91
+ self.icPointSourceId = new IC(16, 1);
92
+ self.icDx = new IC(32, 2);
93
+ self.icDy = new IC(32, 22);
94
+ self.icZ = new IC(32, 20);
95
+ }
96
+
69
97
  /** Point10 v2 decompressor: one per chunk. */
70
98
  export function Point10Decompressor() {
71
- this.size = 20;
72
- this.lastIntensity = new Uint16Array(16);
73
- this.xMedian = medians();
74
- this.yMedian = medians();
75
- this.lastHeight = new Int32Array(8);
76
- this.changedValues = new ArithmeticModel(64);
77
- this.scanAngleRank = models(2, 256);
78
- this.bitByte = new Array(256);
79
- this.classification = new Array(256);
80
- this.userData = new Array(256);
81
- this.icIntensity = new IntegerDecompressor(16, 4);
82
- this.icPointSourceId = new IntegerDecompressor(16, 1);
83
- this.icDx = new IntegerDecompressor(32, 2);
84
- this.icDy = new IntegerDecompressor(32, 22);
85
- this.icZ = new IntegerDecompressor(32, 20);
99
+ initPoint10(this, IntegerDecompressor);
86
100
  }
87
101
 
88
102
  /** The first point of a chunk is stored raw. */
@@ -125,12 +139,10 @@ Point10Decompressor.prototype.next = function (dec, view, o) {
125
139
  var diff = this.icDx.decompress(dec, this.xMedian[m].get(), single);
126
140
  this.x = (this.x + diff) | 0;
127
141
  this.xMedian[m].add(diff);
128
- var kx = this.icDx.k;
129
- diff = this.icDy.decompress(dec, this.yMedian[m].get(), single + (kx < 20 ? kx & 0xFFFFFFFE : 20));
142
+ diff = this.icDy.decompress(dec, this.yMedian[m].get(), yContext(single, this.icDx));
130
143
  this.y = (this.y + diff) | 0;
131
144
  this.yMedian[m].add(diff);
132
- var kz = Math.floor((this.icDx.k + this.icDy.k) / 2);
133
- this.z = this.icZ.decompress(dec, this.lastHeight[l], single + (kz < 18 ? kz & 0xFFFFFFFE : 18));
145
+ this.z = this.icZ.decompress(dec, this.lastHeight[l], zContext(single, this.icDx, this.icDy));
134
146
  this.lastHeight[l] = this.z;
135
147
  view.setInt32(o, this.x, LE); view.setInt32(o + 4, this.y, LE); view.setInt32(o + 8, this.z, LE);
136
148
  view.setUint16(o + 12, this.intensity, LE);
@@ -140,3 +152,60 @@ Point10Decompressor.prototype.next = function (dec, view, o) {
140
152
  view.setUint8(o + 17, this.user);
141
153
  view.setUint16(o + 18, this.psid, LE);
142
154
  };
155
+
156
+ // A DataView over the caller's records, made once per buffer
157
+ function viewOf(item, src) {
158
+ if (item._src !== src) { item._src = src; item._view = new DataView(src.buffer, src.byteOffset, src.byteLength); }
159
+ return item._view;
160
+ }
161
+
162
+ /** Point10 v2 compressor: one per chunk, the mirror of Point10Decompressor. */
163
+ export function Point10Compressor() {
164
+ initPoint10(this, IntegerCompressor);
165
+ this._src = null;
166
+ this._view = null;
167
+ }
168
+
169
+ /** The first point of a chunk is stored raw; it only sets the state. */
170
+ Point10Compressor.prototype.first = function (src, s) {
171
+ var v = viewOf(this, src);
172
+ this.x = v.getInt32(s, LE); this.y = v.getInt32(s + 4, LE); this.z = v.getInt32(s + 8, LE);
173
+ this.bits = v.getUint8(s + 14);
174
+ this.cls = v.getUint8(s + 15);
175
+ this.angle = v.getInt8(s + 16);
176
+ this.user = v.getUint8(s + 17);
177
+ this.psid = v.getUint16(s + 18, LE);
178
+ };
179
+
180
+ Point10Compressor.prototype.next = function (enc, src, s) {
181
+ var v = viewOf(this, src);
182
+ var x = v.getInt32(s, LE), y = v.getInt32(s + 4, LE), z = v.getInt32(s + 8, LE);
183
+ var intensity = v.getUint16(s + 12, LE), bits = v.getUint8(s + 14), cls = v.getUint8(s + 15);
184
+ var angle = v.getInt8(s + 16), user = v.getUint8(s + 17), psid = v.getUint16(s + 18, LE);
185
+ var r = bits & 7, n = (bits >>> 3) & 7;
186
+ var m = NUMBER_RETURN_MAP[n][r], l = NUMBER_RETURN_LEVEL[n][r];
187
+ // return number, number of returns, scan direction and edge fill the bit byte, so "any changed" is the byte changed
188
+ var changed = (bits !== this.bits ? 32 : 0) | (intensity !== this.lastIntensity[m] ? 16 : 0) | (cls !== this.cls ? 8 : 0) |
189
+ (angle !== this.angle ? 4 : 0) | (user !== this.user ? 2 : 0) | (psid !== this.psid ? 1 : 0);
190
+ enc.encodeSymbol(this.changedValues, changed);
191
+ if (changed & 32) enc.encodeSymbol(lazyModel(this.bitByte, this.bits), bits);
192
+ if (changed & 16) {
193
+ this.icIntensity.compress(enc, this.lastIntensity[m], intensity, m < 3 ? m : 3);
194
+ this.lastIntensity[m] = intensity;
195
+ }
196
+ if (changed & 8) enc.encodeSymbol(lazyModel(this.classification, this.cls), cls);
197
+ if (changed & 4) enc.encodeSymbol(this.scanAngleRank[(bits >>> 6) & 1], (angle - this.angle) & 255); // u8 wrapping_sub
198
+ if (changed & 2) enc.encodeSymbol(lazyModel(this.userData, this.user), user);
199
+ if (changed & 1) this.icPointSourceId.compress(enc, this.psid, psid, 0);
200
+ var single = n === 1 ? 1 : 0;
201
+ var diff = (x - this.x) | 0; // i32 wrapping_sub
202
+ this.icDx.compress(enc, this.xMedian[m].get(), diff, single);
203
+ this.xMedian[m].add(diff);
204
+ diff = (y - this.y) | 0;
205
+ this.icDy.compress(enc, this.yMedian[m].get(), diff, yContext(single, this.icDx));
206
+ this.yMedian[m].add(diff);
207
+ this.icZ.compress(enc, this.lastHeight[l], z, zContext(single, this.icDx, this.icDy));
208
+ this.lastHeight[l] = z;
209
+ this.x = x; this.y = y; this.z = z;
210
+ this.bits = bits; this.cls = cls; this.angle = angle; this.user = user; this.psid = psid;
211
+ };
@@ -3,13 +3,15 @@
3
3
  // LASzip RGB12 item, version 2: the 6 colour bytes. A symbol says which
4
4
  // bytes changed and whether green and blue differ from red; red's byte
5
5
  // differences predict green's, and the mean of red's and green's
6
- // predicts blue's (clamped to a byte). Shared by the 1.4 RGB14 item.
6
+ // predicts blue's (clamped to a byte). Shared by the 1.4 RGB14 item;
7
+ // encodeRgb is decodeRgb's mirror, over the same RgbModels.
7
8
  //
8
9
  // Ported from laz-rs src/las/rgb.rs, module v2 (https://github.com/laz-rs/laz-rs),
9
10
  // itself a port of LASzip by rapidlasso. (c) 2007-2014 martin isenburg,
10
11
  // rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
11
12
  // Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
12
- // NOTICE). Changes: translated from Rust to JavaScript, Brent Buffham 2026.
13
+ // NOTICE). Changes: translated from Rust to JavaScript, Brent Buffham 2026;
14
+ // the compressor (encodeRgb, Rgb12Compressor) added beside the decompressor.
13
15
  //==========================================================
14
16
 
15
17
  import { ArithmeticModel } from "../models.js";
@@ -61,13 +63,16 @@ export function decodeRgb(dec, m, last) {
61
63
  last[0] = r; last[1] = g; last[2] = b;
62
64
  }
63
65
 
64
- /** RGB12 v2 decompressor: one per chunk. */
65
- export function Rgb12Decompressor() {
66
- this.size = 6;
67
- this.models = new RgbModels();
68
- this.last = new Uint16Array(3);
66
+ // The state and models both halves start a chunk with
67
+ function initRgb12(self) {
68
+ self.size = 6;
69
+ self.models = new RgbModels();
70
+ self.last = new Uint16Array(3);
69
71
  }
70
72
 
73
+ /** RGB12 v2 decompressor: one per chunk. */
74
+ export function Rgb12Decompressor() { initRgb12(this); }
75
+
71
76
  Rgb12Decompressor.prototype.first = function (src, s, view, o) {
72
77
  for (var i = 0; i < 6; i++) view.setUint8(o + i, src[s + i]);
73
78
  this.last[0] = view.getUint16(o, LE); this.last[1] = view.getUint16(o + 2, LE); this.last[2] = view.getUint16(o + 4, LE);
@@ -77,3 +82,53 @@ Rgb12Decompressor.prototype.next = function (dec, view, o) {
77
82
  decodeRgb(dec, this.models, this.last);
78
83
  view.setUint16(o, this.last[0], LE); view.setUint16(o + 2, this.last[1], LE); view.setUint16(o + 4, this.last[2], LE);
79
84
  };
85
+
86
+ /**
87
+ * Encode one colour against the last one: the mirror of decodeRgb.
88
+ * @param {object} enc - ArithmeticEncoder
89
+ * @param {RgbModels} m
90
+ * @param {Uint16Array} last - [r, g, b] of the last point (not changed)
91
+ * @param {Uint16Array} cur - [r, g, b] of this point
92
+ * @returns {number} the symbol coded: which bytes changed, and bit 6 when it is not grey
93
+ */
94
+ export function encodeRgb(enc, m, last, cur) {
95
+ var lr = last[0], lg = last[1], lb = last[2], r = cur[0], g = cur[1], b = cur[2];
96
+ var sym = ((lr ^ r) & 255 ? 1 : 0) | ((lr ^ r) & 0xFF00 ? 2 : 0) | ((lg ^ g) & 255 ? 4 : 0) | ((lg ^ g) & 0xFF00 ? 8 : 0) |
97
+ ((lb ^ b) & 255 ? 16 : 0) | ((lb ^ b) & 0xFF00 ? 32 : 0) | (r !== g || r !== b ? 64 : 0);
98
+ var diffL = 0, diffH = 0;
99
+ enc.encodeSymbol(m.byteUsed, sym);
100
+ if (sym & 1) { diffL = (r & 255) - (lr & 255); enc.encodeSymbol(m.lowRed, diffL & 255); }
101
+ if (sym & 2) { diffH = (r >>> 8) - (lr >>> 8); enc.encodeSymbol(m.highRed, diffH & 255); }
102
+ if (sym & 64) {
103
+ if (sym & 4) enc.encodeSymbol(m.lowGreen, ((g & 255) - clamp8(diffL + (lg & 255))) & 255);
104
+ if (sym & 16) {
105
+ diffL = Math.trunc((diffL + (g & 255) - (lg & 255)) / 2);
106
+ enc.encodeSymbol(m.lowBlue, ((b & 255) - clamp8(diffL + (lb & 255))) & 255);
107
+ }
108
+ if (sym & 8) enc.encodeSymbol(m.highGreen, ((g >>> 8) - clamp8(diffH + (lg >>> 8))) & 255);
109
+ if (sym & 32) {
110
+ diffH = Math.trunc((diffH + (g >>> 8) - (lg >>> 8)) / 2);
111
+ enc.encodeSymbol(m.highBlue, ((b >>> 8) - clamp8(diffH + (lb >>> 8))) & 255);
112
+ }
113
+ }
114
+ return sym;
115
+ }
116
+
117
+ /** RGB12 v2 compressor: one per chunk, the mirror of Rgb12Decompressor. */
118
+ export function Rgb12Compressor() {
119
+ initRgb12(this);
120
+ this.cur = new Uint16Array(3);
121
+ }
122
+
123
+ /** [r, g, b] of the record bytes at s, into out (RGB14 reads them the same way). */
124
+ export function readRgb(out, src, s) {
125
+ out[0] = src[s] + src[s + 1] * 256; out[1] = src[s + 2] + src[s + 3] * 256; out[2] = src[s + 4] + src[s + 5] * 256;
126
+ }
127
+
128
+ Rgb12Compressor.prototype.first = function (src, s) { readRgb(this.last, src, s); };
129
+
130
+ Rgb12Compressor.prototype.next = function (enc, src, s) {
131
+ readRgb(this.cur, src, s);
132
+ encodeRgb(enc, this.models, this.last, this.cur);
133
+ var t = this.last; this.last = this.cur; this.cur = t;
134
+ };