@asterisk-labs/rumi 0.0.0-stage → 0.27.0-alpha.2

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/header.js ADDED
@@ -0,0 +1,334 @@
1
+ import { dtypeFromSample } from "./dtypes.js";
2
+
3
+ const MAGIC = 0x45564f4c;
4
+ const VERSION = 1;
5
+ const HEADER_SIZE = 32;
6
+ const MAX_U32 = 0xffffffff;
7
+ const MAX_I32 = 0x7fffffff;
8
+ const MAX_U64 = (1n << 64n) - 1n;
9
+ const MAX_INDEX_BYTES = 64 * 1024 * 1024;
10
+ const INDEX_BYTES_PER_FRAME = 12;
11
+
12
+ const AXIS_BAND = "band";
13
+ const AXIS_TIME = "time";
14
+ const AXIS_H = "y";
15
+ const AXIS_W = "x";
16
+
17
+ const units = [
18
+ { axes: [AXIS_H, AXIS_W], timeFirst: false },
19
+ { axes: ["one", AXIS_H, AXIS_W], timeFirst: false },
20
+ { axes: [AXIS_H, AXIS_W, "one"], timeFirst: false },
21
+ { axes: [AXIS_BAND, AXIS_TIME, AXIS_H, AXIS_W], timeFirst: false },
22
+ { axes: [AXIS_TIME, AXIS_BAND, AXIS_H, AXIS_W], timeFirst: false },
23
+ { axes: [AXIS_BAND, AXIS_H, AXIS_W, AXIS_TIME], timeFirst: false },
24
+ { axes: [AXIS_TIME, AXIS_H, AXIS_W, AXIS_BAND], timeFirst: false },
25
+ { axes: [AXIS_H, AXIS_W, AXIS_BAND, AXIS_TIME], timeFirst: false },
26
+ { axes: [AXIS_H, AXIS_W, AXIS_TIME, AXIS_BAND], timeFirst: false },
27
+ { axes: [AXIS_H, AXIS_W], timeFirst: true },
28
+ ];
29
+
30
+ function asBytes(value) {
31
+ if (value instanceof Uint8Array) return value;
32
+ if (value instanceof ArrayBuffer
33
+ || (typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer)) {
34
+ return new Uint8Array(value);
35
+ }
36
+ if (ArrayBuffer.isView(value)) {
37
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
38
+ }
39
+ throw new TypeError("header must be an ArrayBuffer or typed array");
40
+ }
41
+
42
+ function checkedNumber(value, label) {
43
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
44
+ throw new RangeError(`${label} exceeds JavaScript's safe integer range`);
45
+ }
46
+ return Number(value);
47
+ }
48
+
49
+ function externalValueBytes(count, bytesPerValue) {
50
+ const bytes = BigInt(count) * BigInt(bytesPerValue);
51
+ return bytes > 8n ? bytes + (bytes & 1n) : 0n;
52
+ }
53
+
54
+ export function derivedBaseOffset(bands, frames) {
55
+ return 16n + 276n
56
+ + externalValueBytes(16, 8)
57
+ + externalValueBytes(16, 2)
58
+ + externalValueBytes(bands, 2)
59
+ + externalValueBytes(bands, 2)
60
+ + externalValueBytes(frames, 8)
61
+ + externalValueBytes(frames, 4);
62
+ }
63
+
64
+ export function makeExternalHeader(fields, counts) {
65
+ if (counts.length === 0) throw new RangeError("a Rumi file must contain frames");
66
+ let countMin = counts[0];
67
+ for (const count of counts) {
68
+ if (!Number.isInteger(count) || count <= 0 || count > MAX_U32) {
69
+ throw new RangeError("frame byte counts must be positive uint32 values");
70
+ }
71
+ countMin = Math.min(countMin, count);
72
+ }
73
+ let residualOr = 0n;
74
+ for (const count of counts) residualOr |= BigInt(count - countMin);
75
+ const countBits = bitWidth(residualOr);
76
+ const packedLength = Math.ceil(counts.length * countBits / 8);
77
+ const bytes = new Uint8Array(HEADER_SIZE + packedLength);
78
+ const view = new DataView(bytes.buffer);
79
+ view.setUint32(0, MAGIC, true);
80
+ view.setUint16(4, VERSION, true);
81
+ view.setUint32(6, fields.width, true);
82
+ view.setUint32(10, fields.height, true);
83
+ view.setUint32(14, fields.timeCount, true);
84
+ view.setUint16(18, fields.tileWidth, true);
85
+ view.setUint16(20, fields.tileHeight, true);
86
+ view.setUint16(22, fields.bands, true);
87
+ view.setUint8(24, fields.bits);
88
+ view.setUint8(25, fields.sampleFormat);
89
+ view.setUint8(26, fields.frameUnit);
90
+ view.setUint32(27, countMin, true);
91
+ view.setUint8(31, countBits);
92
+
93
+ for (let index = 0; index < counts.length; index += 1) {
94
+ const at = BigInt(index * countBits);
95
+ const first = Number(at >> 3n);
96
+ const word = BigInt(counts[index] - countMin) << (at & 7n);
97
+ for (let byte = 0; byte < 5 && first + byte < packedLength; byte += 1) {
98
+ bytes[HEADER_SIZE + first + byte] |= Number((word >> BigInt(byte * 8)) & 0xffn);
99
+ }
100
+ }
101
+ return bytes;
102
+ }
103
+
104
+ function resolvedAxes(unit, bands, times) {
105
+ const row = units[unit];
106
+ if (!row) return undefined;
107
+ const present = Number(bands > 1) + Number(times > 1);
108
+ if (unit !== 0 && unit !== 1 && unit !== 2 && present !== 2) return undefined;
109
+ if ((unit === 1 || unit === 2) && present !== 1) return undefined;
110
+ if (bands === 0 || times === 0) return undefined;
111
+ const only = bands > 1 ? AXIS_BAND : AXIS_TIME;
112
+ return row.axes.map((axis) => axis === "one" ? only : axis);
113
+ }
114
+
115
+ function product(values, label) {
116
+ let out = 1n;
117
+ for (const value of values) {
118
+ out *= BigInt(value);
119
+ if (out > MAX_U64) throw new RangeError(`${label} overflows uint64`);
120
+ }
121
+ return out;
122
+ }
123
+
124
+ function bitWidth(value) {
125
+ let bits = 0;
126
+ while (value !== 0n) {
127
+ value >>= 1n;
128
+ bits += 1;
129
+ }
130
+ return bits;
131
+ }
132
+
133
+ function unpackResidual(packed, index, width) {
134
+ if (width === 0) return 0n;
135
+ const at = BigInt(index) * BigInt(width);
136
+ const first = Number(at >> 3n);
137
+ const shift = Number(at & 7n);
138
+ let word = 0n;
139
+ const available = Math.min(5, packed.length - first);
140
+ for (let i = 0; i < available; i += 1) {
141
+ word |= BigInt(packed[first + i]) << BigInt(8 * i);
142
+ }
143
+ return (word >> BigInt(shift)) & ((1n << BigInt(width)) - 1n);
144
+ }
145
+
146
+ function frameIndexOf(header, spatial, band, time) {
147
+ let index = spatial;
148
+ const indexedBand = !header.frameAxes.includes(AXIS_BAND);
149
+ const indexedTime = !header.frameAxes.includes(AXIS_TIME);
150
+ const order = header.timeFirst ? [AXIS_TIME, AXIS_BAND] : [AXIS_BAND, AXIS_TIME];
151
+ for (const axis of order) {
152
+ if (axis === AXIS_BAND && indexedBand) index = index * header.bands + band;
153
+ if (axis === AXIS_TIME && indexedTime) index = index * header.timeCount + time;
154
+ }
155
+ return index;
156
+ }
157
+
158
+ export function parseHeader(value, { maxFrameBytes = 1 << 30 } = {}) {
159
+ const bytes = asBytes(value);
160
+ if (!Number.isSafeInteger(maxFrameBytes) || maxFrameBytes <= 0) {
161
+ throw new RangeError("maxFrameBytes must be a positive safe integer");
162
+ }
163
+ if (bytes.byteLength < HEADER_SIZE) throw new RangeError("header is shorter than 32 bytes");
164
+
165
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
166
+ if (view.getUint32(0, true) !== MAGIC) throw new Error("bad Rumi header magic");
167
+ if (view.getUint16(4, true) !== VERSION) throw new Error("unsupported Rumi header version");
168
+
169
+ const width = view.getUint32(6, true);
170
+ const height = view.getUint32(10, true);
171
+ const timeCount = view.getUint32(14, true);
172
+ const tileWidth = view.getUint16(18, true);
173
+ const tileHeight = view.getUint16(20, true);
174
+ const bands = view.getUint16(22, true);
175
+ const bits = view.getUint8(24);
176
+ const sampleFormat = view.getUint8(25);
177
+ const frameUnit = view.getUint8(26);
178
+ const countMin = view.getUint32(27, true);
179
+ const countBits = view.getUint8(31);
180
+
181
+ const dtype = dtypeFromSample(sampleFormat, bits);
182
+ if (!dtype) throw new Error(`unsupported sample format ${sampleFormat}/${bits}`);
183
+ if (width === 0 || height === 0 || timeCount === 0 || tileWidth === 0
184
+ || tileHeight === 0 || bands === 0 || width > MAX_I32 || height > MAX_I32) {
185
+ throw new RangeError("invalid Rumi dimensions");
186
+ }
187
+ const frameAxes = resolvedAxes(frameUnit, bands, timeCount);
188
+ if (!frameAxes) throw new Error("frameUnit does not fit the band and time dimensions");
189
+ if (countBits > 32) throw new RangeError("countBits is greater than 32");
190
+
191
+ const tilesAcross = Math.ceil(width / tileWidth);
192
+ const tilesDown = Math.ceil(height / tileHeight);
193
+ const holdsBand = frameAxes.includes(AXIS_BAND);
194
+ const holdsTime = frameAxes.includes(AXIS_TIME);
195
+ const frameCountBig = product([
196
+ tilesAcross,
197
+ tilesDown,
198
+ holdsBand ? 1 : bands,
199
+ holdsTime ? 1 : timeCount,
200
+ ], "frame count");
201
+ if (frameCountBig > BigInt(MAX_U32)) throw new RangeError("frame count exceeds uint32");
202
+ const frameCount = Number(frameCountBig);
203
+
204
+ const packedLengthBig = (frameCountBig * BigInt(countBits) + 7n) >> 3n;
205
+ const packedLength = checkedNumber(packedLengthBig, "packed frame index");
206
+ if (bytes.byteLength !== HEADER_SIZE + packedLength) {
207
+ throw new RangeError("header size does not match the frame index");
208
+ }
209
+
210
+ const nominalWidth = Math.min(width, tileWidth);
211
+ const nominalHeight = Math.min(height, tileHeight);
212
+ const maxDecodedFrame = product([
213
+ nominalWidth,
214
+ nominalHeight,
215
+ dtype.bytes,
216
+ holdsBand ? bands : 1,
217
+ holdsTime ? timeCount : 1,
218
+ ], "decoded frame size");
219
+ if (maxDecodedFrame > BigInt(maxFrameBytes)) {
220
+ throw new RangeError("decoded frame exceeds maxFrameBytes");
221
+ }
222
+
223
+ const baseFrameOffset = derivedBaseOffset(bands, frameCountBig);
224
+ const tooLargeToExpand = frameCount > Math.floor(MAX_INDEX_BYTES / INDEX_BYTES_PER_FRAME);
225
+ if (tooLargeToExpand && countBits !== 0) {
226
+ throw new RangeError("expanded frame index exceeds 64 MiB");
227
+ }
228
+ if (countBits === 0 && countMin === 0) throw new RangeError("frame byte count is zero");
229
+
230
+ let frameByteCounts;
231
+ let frameOffsets;
232
+ if (countBits !== 0 || !tooLargeToExpand) {
233
+ frameByteCounts = new Uint32Array(frameCount);
234
+ frameOffsets = new BigUint64Array(frameCount);
235
+ const packed = bytes.subarray(HEADER_SIZE);
236
+ let offset = baseFrameOffset;
237
+ let seenLow;
238
+ let seenAny = 0n;
239
+
240
+ for (let i = 0; i < frameCount; i += 1) {
241
+ const residual = unpackResidual(packed, i, countBits);
242
+ seenLow = seenLow === undefined || residual < seenLow ? residual : seenLow;
243
+ seenAny |= residual;
244
+ const count = BigInt(countMin) + residual;
245
+ if (count === 0n) throw new RangeError("frame byte count is zero");
246
+ if (count > BigInt(MAX_U32)) throw new RangeError("frame byte count exceeds uint32");
247
+ if (offset > MAX_U64 - count) throw new RangeError("frame offsets overflow uint64");
248
+ frameByteCounts[i] = Number(count);
249
+ frameOffsets[i] = offset;
250
+ offset += count;
251
+ }
252
+
253
+ if (seenLow !== 0n || bitWidth(seenAny) !== countBits) {
254
+ throw new Error("frame byte counts are not packed canonically");
255
+ }
256
+ const spare = packedLength * 8 - frameCount * countBits;
257
+ if (spare !== 0 && (packed[packedLength - 1] >> (8 - spare)) !== 0) {
258
+ throw new Error("frame byte count padding is not zero");
259
+ }
260
+ } else if (baseFrameOffset > MAX_U64 - frameCountBig * BigInt(countMin)) {
261
+ throw new RangeError("frame offsets overflow uint64");
262
+ }
263
+
264
+ const header = {
265
+ width,
266
+ height,
267
+ timeCount,
268
+ tileWidth,
269
+ tileHeight,
270
+ bands,
271
+ dtype,
272
+ frameUnit,
273
+ frameAxes: Object.freeze(frameAxes),
274
+ frameLayout: frameAxes.map((axis) => ({ band: "b", time: "t", y: "h", x: "w" })[axis]).join(" "),
275
+ timeFirst: units[frameUnit].timeFirst,
276
+ tilesAcross,
277
+ tilesDown,
278
+ frameCount,
279
+ maxDecodedFrame: Number(maxDecodedFrame),
280
+ baseFrameOffset,
281
+ countMin,
282
+ countBits,
283
+ frameOffset(index) {
284
+ if (!Number.isInteger(index) || index < 0 || index >= frameCount) {
285
+ throw new RangeError("frame index is out of range");
286
+ }
287
+ if (frameOffsets) return frameOffsets[index];
288
+ return baseFrameOffset + BigInt(index) * BigInt(countMin);
289
+ },
290
+ frameByteCount(index) {
291
+ if (!Number.isInteger(index) || index < 0 || index >= frameCount) {
292
+ throw new RangeError("frame index is out of range");
293
+ }
294
+ return frameByteCounts ? frameByteCounts[index] : countMin;
295
+ },
296
+ frameIndex(spatial, band, time) {
297
+ return frameIndexOf(header, spatial, band, time);
298
+ },
299
+ };
300
+ return Object.freeze(header);
301
+ }
302
+
303
+ export function unitPixelStep(header) {
304
+ let step = 1;
305
+ let afterWidth = false;
306
+ for (const axis of header.frameAxes) {
307
+ if (axis === AXIS_W) {
308
+ afterWidth = true;
309
+ } else if (afterWidth && axis === AXIS_BAND) {
310
+ step *= header.bands;
311
+ } else if (afterWidth && axis === AXIS_TIME) {
312
+ step *= header.timeCount;
313
+ }
314
+ }
315
+ return step;
316
+ }
317
+
318
+ export function unitPlaneOffset(header, band, time, height, width) {
319
+ let stride = 1;
320
+ let offset = 0;
321
+ for (let i = header.frameAxes.length - 1; i >= 0; i -= 1) {
322
+ const axis = header.frameAxes[i];
323
+ if (axis === AXIS_BAND) {
324
+ offset += band * stride;
325
+ stride *= header.bands;
326
+ } else if (axis === AXIS_TIME) {
327
+ offset += time * stride;
328
+ stride *= header.timeCount;
329
+ } else {
330
+ stride *= axis === AXIS_H ? height : width;
331
+ }
332
+ }
333
+ return offset;
334
+ }
package/src/reader.js ADDED
@@ -0,0 +1,368 @@
1
+ import { storageView } from "./dtypes.js";
2
+ import { externalHeaderFromFile } from "./file_header.js";
3
+ import { parseHeader, unitPixelStep, unitPlaneOffset } from "./header.js";
4
+ import { readExact, sourceFrom } from "./source.js";
5
+
6
+ const DEFAULT_MAX_OUTPUT_BYTES = 1 << 30;
7
+ const DEFAULT_MAX_METADATA_BYTES = 64 * 1024 * 1024;
8
+ const DEFAULT_MAX_TIME_COORDINATES = 1_000_000;
9
+ const MAX_INDEX_FRAMES = Math.floor((64 * 1024 * 1024) / 12);
10
+ const MAX_PARALLEL_READS = 8;
11
+ let sharedDecoder;
12
+
13
+ function checkedProduct(values, label) {
14
+ let result = 1;
15
+ for (const value of values) {
16
+ if (!Number.isSafeInteger(value) || value < 0 || (value !== 0 && result > Number.MAX_SAFE_INTEGER / value)) {
17
+ throw new RangeError(`${label} exceeds JavaScript's safe integer range`);
18
+ }
19
+ result *= value;
20
+ }
21
+ return result;
22
+ }
23
+
24
+ function validateLimit(value, label) {
25
+ if (!Number.isSafeInteger(value) || value <= 0) {
26
+ throw new RangeError(`${label} must be a positive safe integer`);
27
+ }
28
+ }
29
+
30
+ async function forEachConcurrent(values, callback) {
31
+ let next = 0;
32
+ let stopped = false;
33
+ async function worker() {
34
+ while (!stopped) {
35
+ const index = next;
36
+ next += 1;
37
+ if (index >= values.length) return;
38
+ try {
39
+ await callback(values[index]);
40
+ } catch (error) {
41
+ stopped = true;
42
+ throw error;
43
+ }
44
+ }
45
+ }
46
+ const count = Math.min(MAX_PARALLEL_READS, values.length);
47
+ await Promise.all(Array.from({ length: count }, worker));
48
+ }
49
+
50
+ function selection(value, extent, label) {
51
+ if (value === undefined) {
52
+ return { length: extent, uniqueLength: extent, at: (index) => index };
53
+ }
54
+ if (!Array.isArray(value) && !ArrayBuffer.isView(value)) {
55
+ throw new TypeError(`${label} must be an array of zero-based indices`);
56
+ }
57
+ const out = Array.from(value);
58
+ if (out.length === 0) throw new RangeError(`${label} cannot be empty`);
59
+ for (const index of out) {
60
+ if (!Number.isSafeInteger(index) || index < 0 || index >= extent) {
61
+ throw new RangeError(`${label} index ${index} is out of range`);
62
+ }
63
+ }
64
+ return {
65
+ length: out.length,
66
+ uniqueLength: new Set(out).size,
67
+ at: (index) => out[index],
68
+ };
69
+ }
70
+
71
+ function windowOf(value, header) {
72
+ if (value === undefined) return [0, 0, header.height, header.width];
73
+ if (!Array.isArray(value) || value.length !== 4) {
74
+ throw new TypeError("window must be [y, x, height, width]");
75
+ }
76
+ const [y, x, height, width] = value;
77
+ for (const part of value) {
78
+ if (!Number.isSafeInteger(part) || part < 0) {
79
+ throw new RangeError("window values must be non-negative safe integers");
80
+ }
81
+ }
82
+ if (height === 0 || width === 0) throw new RangeError("window cannot be empty");
83
+ if (y > header.height - height || x > header.width - width) {
84
+ throw new RangeError("window extends past the image");
85
+ }
86
+ return [y, x, height, width];
87
+ }
88
+
89
+ function requestOf(options, header) {
90
+ const times = selection(options.time, header.timeCount, "time");
91
+ const bands = selection(options.bands, header.bands, "bands");
92
+ if (header.timeCount === 1 && times.length !== 1) {
93
+ throw new RangeError("an Image time selection must contain one index");
94
+ }
95
+ const window = windowOf(options.window, header);
96
+ return { times, bands, window };
97
+ }
98
+
99
+ function requestedFrameCount(header, request) {
100
+ const [y, x, height, width] = request.window;
101
+ const tileRows = Math.ceil((y + height) / header.tileHeight) - Math.floor(y / header.tileHeight);
102
+ const tileColumns = Math.ceil((x + width) / header.tileWidth) - Math.floor(x / header.tileWidth);
103
+ return checkedProduct([
104
+ tileRows,
105
+ tileColumns,
106
+ header.frameAxes.includes("band") ? 1 : request.bands.uniqueLength,
107
+ header.frameAxes.includes("time") ? 1 : request.times.uniqueLength,
108
+ ], "planned frame count");
109
+ }
110
+
111
+ function frameGroups(header, request) {
112
+ const [y, x, height, width] = request.window;
113
+ const ty0 = Math.floor(y / header.tileHeight);
114
+ const tx0 = Math.floor(x / header.tileWidth);
115
+ const ty1 = Math.ceil((y + height) / header.tileHeight);
116
+ const tx1 = Math.ceil((x + width) / header.tileWidth);
117
+ const groups = new Map();
118
+
119
+ for (let ty = ty0; ty < ty1; ty += 1) {
120
+ for (let tx = tx0; tx < tx1; tx += 1) {
121
+ const spatial = ty * header.tilesAcross + tx;
122
+ for (let outTime = 0; outTime < request.times.length; outTime += 1) {
123
+ const time = request.times.at(outTime);
124
+ for (let outBand = 0; outBand < request.bands.length; outBand += 1) {
125
+ const band = request.bands.at(outBand);
126
+ const frameIndex = header.frameIndex(spatial, band, time);
127
+ let group = groups.get(frameIndex);
128
+ if (!group) {
129
+ group = { frameIndex, ty, tx, planes: [] };
130
+ groups.set(frameIndex, group);
131
+ }
132
+ group.planes.push(time, band, outTime, outBand);
133
+ }
134
+ }
135
+ }
136
+ }
137
+ return [...groups.values()].sort((a, b) => a.frameIndex - b.frameIndex);
138
+ }
139
+
140
+ function decoderFunction(decoder) {
141
+ if (typeof decoder === "function") return decoder;
142
+ if (decoder && typeof decoder.decompress === "function") {
143
+ return (frame, options) => decoder.decompress(frame, options);
144
+ }
145
+ throw new TypeError("decoder must be a function or expose decompress()");
146
+ }
147
+
148
+ function defaultDecoder(options) {
149
+ let decoder;
150
+ return () => {
151
+ if (options === undefined && sharedDecoder) return sharedDecoder;
152
+ if (decoder) return decoder;
153
+ const pending = import("@asterisk-labs/geozl")
154
+ .then(({ createGeoZL }) => createGeoZL(options));
155
+ decoder = pending;
156
+ if (options === undefined) sharedDecoder = pending;
157
+ pending.catch(() => {
158
+ if (decoder === pending) decoder = undefined;
159
+ if (sharedDecoder === pending) sharedDecoder = undefined;
160
+ });
161
+ return pending;
162
+ };
163
+ }
164
+
165
+ async function decodedBytes(decode, compressed, expected, verify) {
166
+ const value = await decode(compressed, { verify, maxOutputSize: expected });
167
+ let bytes;
168
+ if (value instanceof Uint8Array) {
169
+ bytes = value;
170
+ } else if (value instanceof ArrayBuffer
171
+ || (typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer)) {
172
+ bytes = new Uint8Array(value);
173
+ } else if (ArrayBuffer.isView(value)) {
174
+ bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
175
+ } else {
176
+ throw new TypeError("decoder must return bytes or a typed array");
177
+ }
178
+ if (bytes.byteLength !== expected) {
179
+ throw new RangeError(`decoded frame has ${bytes.byteLength} bytes, expected ${expected}`);
180
+ }
181
+ return bytes;
182
+ }
183
+
184
+ function validateDecodedSamples(bytes, dtype) {
185
+ if (dtype.bits !== 1) return;
186
+ const invalid = bytes.findIndex((value) => value !== 0 && value !== 1);
187
+ if (invalid !== -1) {
188
+ throw new Error(`decoded bool byte ${invalid} is neither 0 nor 1`);
189
+ }
190
+ }
191
+
192
+ function publicInfo(header, metadata) {
193
+ const axes = header.timeCount === 1 ? ["band", "y", "x"] : ["time", "band", "y", "x"];
194
+ const shape = header.timeCount === 1
195
+ ? [header.bands, header.height, header.width]
196
+ : [header.timeCount, header.bands, header.height, header.width];
197
+ return Object.freeze({
198
+ shape: Object.freeze(shape),
199
+ axes: Object.freeze(axes),
200
+ dtype: header.dtype,
201
+ tileShape: Object.freeze([header.tileHeight, header.tileWidth]),
202
+ gridShape: Object.freeze([header.tilesDown, header.tilesAcross]),
203
+ frameUnit: header.frameUnit,
204
+ frameLayout: header.frameLayout,
205
+ frameCount: header.frameCount,
206
+ transform: metadata.transform,
207
+ epsg: metadata.epsg,
208
+ crs: metadata.crs,
209
+ pixelIsPoint: metadata.pixelIsPoint,
210
+ bands: metadata.bands,
211
+ time: metadata.time,
212
+ timeKind: metadata.timeKind,
213
+ });
214
+ }
215
+
216
+ export class RumiDataset {
217
+ constructor(source, header, decoder, maxOutputBytes, metadata) {
218
+ this.source = source;
219
+ this.header = header;
220
+ this.decoder = decoder;
221
+ this.maxOutputBytes = maxOutputBytes;
222
+ this.metadata = Object.freeze({
223
+ transform: metadata.transform ?? null,
224
+ epsg: metadata.epsg ?? null,
225
+ crs: metadata.crs ?? null,
226
+ pixelIsPoint: metadata.pixelIsPoint ?? false,
227
+ bands: metadata.bands ?? null,
228
+ time: metadata.time ?? null,
229
+ timeKind: metadata.timeKind ?? null,
230
+ });
231
+ }
232
+
233
+ info() {
234
+ return publicInfo(this.header, this.metadata);
235
+ }
236
+
237
+ planRanges(options = {}) {
238
+ const request = requestOf(options, this.header);
239
+ if (requestedFrameCount(this.header, request) > MAX_INDEX_FRAMES) {
240
+ throw new RangeError("planned ranges exceed 64 MiB");
241
+ }
242
+ const groups = frameGroups(this.header, request);
243
+ return groups.map(({ frameIndex }) => Object.freeze({
244
+ frameIndex,
245
+ offset: this.header.frameOffset(frameIndex),
246
+ length: this.header.frameByteCount(frameIndex),
247
+ }));
248
+ }
249
+
250
+ async read(options = {}) {
251
+ const request = requestOf(options, this.header);
252
+ const [, , outHeight, outWidth] = request.window;
253
+ const outputBytes = checkedProduct([
254
+ request.times.length,
255
+ request.bands.length,
256
+ outHeight,
257
+ outWidth,
258
+ this.header.dtype.bytes,
259
+ ], "output size");
260
+ if (outputBytes > this.maxOutputBytes) throw new RangeError("read exceeds maxOutputBytes");
261
+ if (requestedFrameCount(this.header, request) > MAX_INDEX_FRAMES) {
262
+ throw new RangeError("planned ranges exceed 64 MiB");
263
+ }
264
+ const output = new Uint8Array(outputBytes);
265
+ const groups = frameGroups(this.header, request);
266
+ const decode = decoderFunction(await this.decoder());
267
+ const pixelStep = unitPixelStep(this.header);
268
+ const bytesPerSample = this.header.dtype.bytes;
269
+ const [windowY, windowX] = request.window;
270
+
271
+ await forEachConcurrent(groups, async (group) => {
272
+ const tileX = group.tx * this.header.tileWidth;
273
+ const tileY = group.ty * this.header.tileHeight;
274
+ const frameWidth = Math.min(this.header.tileWidth, this.header.width - tileX);
275
+ const frameHeight = Math.min(this.header.tileHeight, this.header.height - tileY);
276
+ const frameBytes = checkedProduct([
277
+ frameWidth,
278
+ frameHeight,
279
+ bytesPerSample,
280
+ this.header.frameAxes.includes("band") ? this.header.bands : 1,
281
+ this.header.frameAxes.includes("time") ? this.header.timeCount : 1,
282
+ ], "decoded frame size");
283
+ if (frameBytes > this.header.maxDecodedFrame) throw new RangeError("decoded frame exceeds its header bound");
284
+
285
+ const compressed = await readExact(
286
+ this.source,
287
+ this.header.frameOffset(group.frameIndex),
288
+ this.header.frameByteCount(group.frameIndex),
289
+ );
290
+ const decoded = await decodedBytes(decode, compressed, frameBytes, options.verify !== false);
291
+ validateDecodedSamples(decoded, this.header.dtype);
292
+
293
+ const ix0 = Math.max(tileX, windowX);
294
+ const iy0 = Math.max(tileY, windowY);
295
+ const ix1 = Math.min(tileX + frameWidth, windowX + outWidth);
296
+ const iy1 = Math.min(tileY + frameHeight, windowY + outHeight);
297
+ const copyWidth = ix1 - ix0;
298
+ const srcX = ix0 - tileX;
299
+ const srcY = iy0 - tileY;
300
+ const dstX = ix0 - windowX;
301
+ const dstY = iy0 - windowY;
302
+
303
+ for (let at = 0; at < group.planes.length; at += 4) {
304
+ const time = group.planes[at];
305
+ const band = group.planes[at + 1];
306
+ const outTime = group.planes[at + 2];
307
+ const outBand = group.planes[at + 3];
308
+ const planeOffset = unitPlaneOffset(
309
+ this.header,
310
+ band,
311
+ time,
312
+ frameHeight,
313
+ frameWidth,
314
+ );
315
+ for (let row = 0; row < iy1 - iy0; row += 1) {
316
+ const srcSample = planeOffset + (srcY + row) * frameWidth * pixelStep + srcX * pixelStep;
317
+ const dstSample = ((outTime * request.bands.length + outBand)
318
+ * outHeight * outWidth) + (dstY + row) * outWidth + dstX;
319
+ if (pixelStep === 1) {
320
+ output.set(
321
+ decoded.subarray(srcSample * bytesPerSample, (srcSample + copyWidth) * bytesPerSample),
322
+ dstSample * bytesPerSample,
323
+ );
324
+ } else {
325
+ for (let column = 0; column < copyWidth; column += 1) {
326
+ const src = (srcSample + column * pixelStep) * bytesPerSample;
327
+ const dst = (dstSample + column) * bytesPerSample;
328
+ output.set(decoded.subarray(src, src + bytesPerSample), dst);
329
+ }
330
+ }
331
+ }
332
+ }
333
+ });
334
+
335
+ const shape = this.header.timeCount === 1
336
+ ? [request.bands.length, outHeight, outWidth]
337
+ : [request.times.length, request.bands.length, outHeight, outWidth];
338
+ const axes = this.header.timeCount === 1 ? ["band", "y", "x"] : ["time", "band", "y", "x"];
339
+ return Object.freeze({
340
+ data: storageView(output, this.header.dtype),
341
+ bytes: output,
342
+ shape: Object.freeze(shape),
343
+ axes: Object.freeze(axes),
344
+ dtype: this.header.dtype,
345
+ });
346
+ }
347
+ }
348
+
349
+ export async function openRumi(value, options = {}) {
350
+ const maxFrameBytes = options.maxFrameBytes ?? (1 << 30);
351
+ const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
352
+ const maxMetadataBytes = options.maxMetadataBytes ?? DEFAULT_MAX_METADATA_BYTES;
353
+ const maxTimeCoordinates = options.maxTimeCoordinates ?? DEFAULT_MAX_TIME_COORDINATES;
354
+ validateLimit(maxFrameBytes, "maxFrameBytes");
355
+ validateLimit(maxOutputBytes, "maxOutputBytes");
356
+ validateLimit(maxMetadataBytes, "maxMetadataBytes");
357
+ validateLimit(maxTimeCoordinates, "maxTimeCoordinates");
358
+ const source = sourceFrom(value, options);
359
+ const rebuilt = await externalHeaderFromFile(source, {
360
+ maxMetadataBytes,
361
+ maxTimeCoordinates,
362
+ });
363
+ const header = parseHeader(rebuilt.header, { maxFrameBytes });
364
+ const decoder = options.decoder === undefined
365
+ ? defaultDecoder(options.geozl)
366
+ : () => Promise.resolve(options.decoder);
367
+ return new RumiDataset(source, header, decoder, maxOutputBytes, rebuilt.metadata);
368
+ }