las-laz-js 0.3.0 → 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.
@@ -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,11 +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)
8
- // Out: { id, records: ArrayBuffer } or { id, error: { code, message, 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)
11
+ // Out: { ready: true } once listening, then per task
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 } }
9
14
  //
10
15
  // Uses the chunk decoder translated from laz-rs (https://github.com/laz-rs/laz-rs),
11
16
  // itself a port of LASzip by rapidlasso. (c) 2007-2015 martin isenburg,
@@ -15,6 +20,7 @@
15
20
  //==========================================================
16
21
 
17
22
  import { createChunkDecoder } from "./decompressChunk.js";
23
+ import { createChunkCompressor } from "./compressChunk.js";
18
24
 
19
25
  // node:worker_threads is named at run time so browser bundlers leave it alone
20
26
  var NODE_WORKER_THREADS = ["node", "worker_threads"].join(":");
@@ -28,24 +34,51 @@ if (typeof self !== "undefined" && typeof self.postMessage === "function") {
28
34
  port = { post: function (m, t) { wt.parentPort.postMessage(m, t); } };
29
35
  wt.parentPort.on("message", handle);
30
36
  }
37
+ port.post({ ready: true }); // the pool waits for this before sending work
31
38
 
32
- 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);
33
49
 
34
50
  function handle(m) {
35
51
  try {
36
- var key = JSON.stringify(m.vlr) + "/" + m.recordLength;
37
- if (cached.key !== key) cached = { key: key, decode: createChunkDecoder(m.vlr, m.recordLength) };
38
- var bytes = new Uint8Array(m.packed);
39
- var total = m.chunks.reduce(function (s, c) { return s + c.count; }, 0);
40
- var out = new ArrayBuffer(total * m.recordLength);
41
- var view = new DataView(out), o = 0;
42
- for (var i = 0; i < m.chunks.length; i++) {
43
- var c = m.chunks[i];
44
- cached.decode(bytes, c.s, c.e, c.count, view, o, m.base);
45
- o += c.count * m.recordLength;
46
- }
47
- port.post({ id: m.id, records: out }, [out]);
52
+ if (m.op === "compress") compress(m);
53
+ else decode(m);
48
54
  } catch (err) {
49
55
  port.post({ id: m.id, error: { code: err && err.code, message: err && err.message ? err.message : String(err), offset: err && err.offset } });
50
56
  }
51
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
+ }
@@ -0,0 +1,107 @@
1
+ // src/laz/compressChunk.js
2
+ //==========================================================
3
+ // Raw LAS records -> one LAZ chunk, the writing half of
4
+ // decompressChunk.js: fresh item compressors per chunk, the first
5
+ // record stored raw, then — point-wise (formats 0-3) — one arithmetic
6
+ // stream for the rest, or — layered (formats 6-8) — the point count,
7
+ // every field's layer sizes and the layers. A 0-point chunk is empty;
8
+ // a point-wise 1-point chunk is the raw record alone, a layered one
9
+ // still has its count, sizes and two empty streams (as LAZperf writes
10
+ // both).
11
+ //
12
+ // Item dispatch after laz-rs src/record.rs and src/laszip/details.rs
13
+ // (https://github.com/laz-rs/laz-rs), itself from LASzip by rapidlasso.
14
+ // (c) 2007-2015 martin isenburg, rapidlasso; (c) 2019-2023 Thomas Montaigu.
15
+ // Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
16
+ // NOTICE). Changes: written in JavaScript over byte buffers, Brent Buffham 2026.
17
+ //==========================================================
18
+
19
+ import { ArithmeticEncoder } from "./ArithmeticEncoder.js";
20
+ import { ITEM, COMPRESSOR } from "./LazVlr.js";
21
+ import { checkChunkVlr, checkPointwiseItem, checkLayeredItem } from "./decompressChunk.js";
22
+ import { Point10Compressor } from "./v2/point10.js";
23
+ import { GpsTime11Compressor } from "./v2/gpstime11.js";
24
+ import { Rgb12Compressor } from "./v2/rgb12.js";
25
+ import { ByteCompressor } from "./v2/byte.js";
26
+ import { Point14Compressor } from "./v3/point14.js";
27
+ import { Rgb14Compressor, Nir14Compressor, Byte14Compressor } from "./v3/layered.js";
28
+
29
+ var LE = true;
30
+
31
+ function pointwiseItem(it) {
32
+ checkPointwiseItem(it);
33
+ switch (it.type) {
34
+ case ITEM.POINT10: return function () { return new Point10Compressor(); };
35
+ case ITEM.GPSTIME11: return function () { return new GpsTime11Compressor(); };
36
+ case ITEM.RGB12: return function () { return new Rgb12Compressor(); };
37
+ default: return function () { return new ByteCompressor(it.size); };
38
+ }
39
+ }
40
+
41
+ // Layered (LAS 1.4) items, versions 3 and 4: each makes one or more field compressors
42
+ function layeredItem(it) {
43
+ checkLayeredItem(it);
44
+ var v = it.version;
45
+ switch (it.type) {
46
+ case ITEM.POINT14: return function () { return [new Point14Compressor(v)]; };
47
+ case ITEM.RGB14: return function () { return [new Rgb14Compressor(v)]; };
48
+ case ITEM.RGBNIR14: return function () { return [new Rgb14Compressor(v), new Nir14Compressor(v)]; };
49
+ default: return function () { return [new Byte14Compressor(it.size, v)]; };
50
+ }
51
+ }
52
+
53
+ // A layered chunk, the mirror of decompressChunk's: the first record raw, a u32 point count,
54
+ // every field's layer sizes (u32), then the layers; Point14 passes the scanner channel on to
55
+ // the fields after it, from 0 at each point as the decoder starts it
56
+ function layeredChunk(makers, recordLength) {
57
+ return function compressChunk(records, start, count) {
58
+ if (!count) return new Uint8Array(0);
59
+ var fields = [];
60
+ makers.forEach(function (make) { fields.push.apply(fields, make()); });
61
+ var ctx = { v: 0 }, p = start, k;
62
+ for (k = 0; k < fields.length; k++) { fields[k].first(records, p, ctx); p += fields[k].size; }
63
+ for (var i = 1; i < count; i++) {
64
+ ctx.v = 0;
65
+ for (k = 0; k < fields.length; k++) { fields[k].next(records, p, ctx); p += fields[k].size; }
66
+ }
67
+ var layers = [];
68
+ fields.forEach(function (f) { layers.push.apply(layers, f.finish()); });
69
+ var total = layers.reduce(function (n, l) { return n + l.length; }, recordLength + 4 + 4 * layers.length);
70
+ var out = new Uint8Array(total), dv = new DataView(out.buffer);
71
+ out.set(records.subarray(start, start + recordLength));
72
+ dv.setUint32(recordLength, count, LE);
73
+ var q = recordLength + 4;
74
+ layers.forEach(function (l) { dv.setUint32(q, l.length, LE); q += 4; });
75
+ layers.forEach(function (l) { out.set(l, q); q += l.length; });
76
+ return out;
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Check a LASzip VLR against the records and build the chunk compressor.
82
+ * @param {object} vlr - parseLazVlr, or the VLR the writer will store
83
+ * @param {number} recordLength - the records' length
84
+ * @returns {function(Uint8Array, number, number): Uint8Array}
85
+ * compress(records, start, count) — the chunk of `count` records from byte `start`,
86
+ * exactly as a LAZ file stores it
87
+ */
88
+ export function createChunkCompressor(vlr, recordLength) {
89
+ checkChunkVlr(vlr, recordLength);
90
+ if (vlr.compressor === COMPRESSOR.LAYERED_CHUNKED) return layeredChunk(vlr.items.map(layeredItem), recordLength);
91
+ var makers = vlr.items.map(pointwiseItem);
92
+ return function compressChunk(records, start, count) {
93
+ if (!count) return new Uint8Array(0);
94
+ var items = makers.map(function (make) { return make(); });
95
+ var k, p = start;
96
+ for (k = 0; k < items.length; k++) { items[k].first(records, p); p += items[k].size; }
97
+ if (count === 1) return records.slice(start, p);
98
+ var enc = new ArithmeticEncoder();
99
+ for (var i = 1; i < count; i++) {
100
+ for (k = 0; k < items.length; k++) { items[k].next(enc, records, p); p += items[k].size; }
101
+ }
102
+ var stream = enc.done(), out = new Uint8Array(recordLength + stream.length);
103
+ out.set(records.subarray(start, start + recordLength));
104
+ out.set(stream, recordLength);
105
+ return out;
106
+ };
107
+ }
@@ -25,31 +25,51 @@ import { Rgb14Decompressor, Nir14Decompressor, Byte14Decompressor } from "./v3/l
25
25
 
26
26
  var NAMES = { 0: "Byte", 6: "Point10", 7: "GpsTime11", 8: "RGB12", 9: "WavePacket13", 10: "Point14", 11: "RGB14", 12: "RGBNIR14", 13: "WavePacket14", 14: "Byte14" };
27
27
 
28
- function pointwiseItem(it) {
28
+ /**
29
+ * Refuse a point-wise item this library cannot code (the compressor shares the check).
30
+ * @param {{type:number, size:number, version:number}} it
31
+ */
32
+ export function checkPointwiseItem(it) {
29
33
  if (it.version !== 2 && !(it.type === ITEM.BYTE && it.version === 1)) {
30
34
  throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ item " + (NAMES[it.type] || it.type) + " version " + it.version + " is not supported (version 2 is; version 1 is from LASzip before 2011)");
31
35
  }
36
+ if (it.type !== ITEM.POINT10 && it.type !== ITEM.GPSTIME11 && it.type !== ITEM.RGB12 && it.type !== ITEM.BYTE) {
37
+ throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ item " + (NAMES[it.type] || it.type) + " is not supported (wave packets are not read)");
38
+ }
39
+ }
40
+
41
+ function pointwiseItem(it) {
42
+ checkPointwiseItem(it);
32
43
  switch (it.type) {
33
44
  case ITEM.POINT10: return function () { return new Point10Decompressor(); };
34
45
  case ITEM.GPSTIME11: return function () { return new GpsTime11Decompressor(); };
35
46
  case ITEM.RGB12: return function () { return new Rgb12Decompressor(); };
36
- case ITEM.BYTE: return function () { return new ByteDecompressor(it.size); };
37
- default: throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ item " + (NAMES[it.type] || it.type) + " is not supported (wave packets are not read)");
47
+ default: return function () { return new ByteDecompressor(it.size); };
38
48
  }
39
49
  }
40
50
 
41
- // Layered (LAS 1.4) items, versions 3 and 4: each makes one or more field decoders
42
- function layeredItem(it) {
51
+ /**
52
+ * Refuse a layered (LAS 1.4) item this library cannot code (the compressor shares the check).
53
+ * @param {{type:number, size:number, version:number}} it
54
+ */
55
+ export function checkLayeredItem(it) {
43
56
  if (it.version !== 3 && it.version !== 4) {
44
57
  throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ item " + (NAMES[it.type] || it.type) + " version " + it.version + " is not supported in a layered file (versions 3 and 4 are)");
45
58
  }
59
+ if (it.type !== ITEM.POINT14 && it.type !== ITEM.RGB14 && it.type !== ITEM.RGBNIR14 && it.type !== ITEM.BYTE14) {
60
+ throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ item " + (NAMES[it.type] || it.type) + " is not supported (wave packets are not read)");
61
+ }
62
+ }
63
+
64
+ // Layered (LAS 1.4) items, versions 3 and 4: each makes one or more field decoders
65
+ function layeredItem(it) {
66
+ checkLayeredItem(it);
46
67
  var v = it.version;
47
68
  switch (it.type) {
48
69
  case ITEM.POINT14: return function () { return [new Point14Decompressor(v)]; };
49
70
  case ITEM.RGB14: return function () { return [new Rgb14Decompressor(v)]; };
50
71
  case ITEM.RGBNIR14: return function () { return [new Rgb14Decompressor(v), new Nir14Decompressor(v)]; };
51
- case ITEM.BYTE14: return function () { return [new Byte14Decompressor(it.size, v)]; };
52
- default: throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ item " + (NAMES[it.type] || it.type) + " is not supported (wave packets are not read)");
72
+ default: return function () { return [new Byte14Decompressor(it.size, v)]; };
53
73
  }
54
74
  }
55
75
 
@@ -81,6 +101,18 @@ function layeredChunk(makers, recordLength, layerWords) {
81
101
  };
82
102
  }
83
103
 
104
+ /**
105
+ * Check that a LASzip VLR's items make the header's records, with the one coder there is
106
+ * (the compressor shares the check).
107
+ * @param {object} vlr - parseLazVlr
108
+ * @param {number} recordLength - header record length
109
+ */
110
+ export function checkChunkVlr(vlr, recordLength) {
111
+ var size = vlr.items.reduce(function (s, it) { return s + it.size; }, 0);
112
+ if (size !== recordLength) throw new LasError(ERROR_CODES.BAD_VLR, "the LASzip items make " + size + "-byte records but the header says " + recordLength);
113
+ if (vlr.coder !== 0) throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ coder " + vlr.coder + " (only the arithmetic coder, 0, exists)");
114
+ }
115
+
84
116
  /**
85
117
  * Check a LASzip VLR against the file and build the chunk decoder.
86
118
  * @param {object} vlr - parseLazVlr
@@ -90,9 +122,7 @@ function layeredChunk(makers, recordLength, layerWords) {
90
122
  * base is the file offset of bytes[0], for error offsets
91
123
  */
92
124
  export function createChunkDecoder(vlr, recordLength) {
93
- var size = vlr.items.reduce(function (s, it) { return s + it.size; }, 0);
94
- if (size !== recordLength) throw new LasError(ERROR_CODES.BAD_VLR, "the LASzip items make " + size + "-byte records but the header says " + recordLength);
95
- if (vlr.coder !== 0) throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ coder " + vlr.coder + " (only the arithmetic coder, 0, exists)");
125
+ checkChunkVlr(vlr, recordLength);
96
126
  if (vlr.compressor === COMPRESSOR.LAYERED_CHUNKED) {
97
127
  var words = vlr.items.reduce(function (s, it) { return s + (it.type === ITEM.POINT14 ? 9 : it.type === ITEM.RGBNIR14 ? 2 : it.type === ITEM.BYTE14 ? it.size : 1); }, 0);
98
128
  return layeredChunk(vlr.items.map(layeredItem), recordLength, words);