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