@ringozz/godot 4.7.2-626 → 4.7.2-639

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/web-image.ts CHANGED
@@ -2,152 +2,329 @@
2
2
  Copyright (c) Vladimir Davidovich. All rights reserved.
3
3
  ***********************************************************************/
4
4
 
5
- // Web-only: decode the embedded PNG/WebP blobs of a `.ctex` CompressedTexture2D
6
- // container with the browser's `createImageBitmap`, and rebuild the container as
7
- // a raw RGBA8 `DATA_FORMAT_IMAGE` ctex — so no image codec runs in wasm on web.
8
- // Anything that isn't a PNG/WebP-embedded ctex is returned unchanged.
5
+ // Web-only compressed-texture (`ctex`) handling. Two container layouts share the
6
+ // same per-image body (`sub-header + level-size table + payloads`):
7
+ // - `GST2` `CompressedTexture2D` (`.ctex`): one body.
8
+ // - `GSTL` `CompressedTexture2DArray` / `Cubemap` / `CubemapArray`
9
+ // (`.ctexarray` / `.ccube` / `.ccubearray`): N bodies of identical geometry.
10
+ // `CompressedTexture3D` (`.ctex3d`) has its own layout and is not handled, and
11
+ // the outer headers are the same size for both (`magic + 8 x u32`), which is why
12
+ // a single parse/allocate pair covers everything — a container is just a list of
13
+ // layers.
9
14
  //
10
- // The mipmap blobs are codec-decoded **concurrently** (Promise.all), then
11
- // rasterized serially into one reused OffscreenCanvas, so wall time is dominated
12
- // by the full-res level's decode instead of the serial sum of every level.
15
+ // Exposes the container/format helpers, `pickGodotFormats` (probing the WebGL2
16
+ // extensions GLES3 checks) and `decodeCtex` — the default staging path, which
17
+ // decodes the PNG/WebP payloads with the browser and rebuilds a raw RGBA8
18
+ // container so no image codec runs in wasm. The optional compressed-transcode
19
+ // hook lives in load.ts.
20
+
21
+ // Both container layouts share the outer header (magic + 8 x u32) and the
22
+ // per-image sub-header, so an image body's data starts at a fixed offset.
23
+ const OUTER_HEADER_SIZE = 36;
24
+ const SUB_HEADER_SIZE = 16;
13
25
 
14
- const CTEX_MAGIC = 'GST2';
15
- const DATA_FORMAT_IMAGE = 0;
16
26
  const DATA_FORMAT_PNG = 1;
17
27
  const DATA_FORMAT_WEBP = 2;
28
+
29
+ // `Image::Format` targets we emit (`core/io/image.h`). Only BC7 and ASTC: both
30
+ // are 4x4/16-byte formats, which is why the size math needs no format table.
31
+ export const FORMAT_BPTC_RGBA = 22;
32
+ export const FORMAT_ASTC_4x4 = 35;
33
+
34
+ // Image-body data starts here: outer header + sub-header.
35
+ export const BLOB_TABLE_OFFSET = OUTER_HEADER_SIZE + SUB_HEADER_SIZE;
36
+
37
+ const DATA_FORMAT_IMAGE = 0;
18
38
  const FORMAT_RGBA8 = 5;
39
+ const RGBA8_PIXEL_SIZE = 4;
19
40
  const FORMAT_BIT_STREAM = 1 << 22;
41
+ const MAGIC_LAYERED = 0x4c; // 'GSTL'; 'GST2' (0x32) is the single-image layout
20
42
 
21
- // Outer header: magic(4) version(4) w(4) h(4) df(4) mipmap_limit(4) reserved(12).
22
- const OUTER_HEADER_SIZE = 36;
23
- // Sub-header: data_format(4) w(2) h(2) mipmaps(4) format(4).
24
- const SUB_HEADER_OFFSET = OUTER_HEADER_SIZE;
25
- const SUB_HEADER_SIZE = 16;
26
- const BLOB_TABLE_OFFSET = SUB_HEADER_OFFSET + SUB_HEADER_SIZE;
27
-
28
- // One encoded mipmap's byte range inside the container, plus the RGBA
29
- // dimensions its decoded pixels must have (`w>>i` clamped to 1).
30
- interface MipEntry {
31
- offset: number;
32
- size: number;
43
+ export interface CtexLayer {
44
+ /** `DATA_FORMAT_PNG` (1) / `DATA_FORMAT_WEBP` (2). */
45
+ dataFormat: number;
33
46
  width: number;
34
47
  height: number;
48
+ /** Highest mipmap index (total levels = `mipmaps + 1`). */
49
+ mipmaps: number;
50
+ /** Per-level payload byte ranges, level 0 first. */
51
+ levels: { offset: number; size: number }[];
35
52
  }
36
53
 
37
- function decodeBitmap(blob: Uint8Array<ArrayBuffer>, type: string): Promise<ImageBitmap> {
38
- return createImageBitmap(new Blob([blob], { type }), {
39
- colorSpaceConversion: 'none',
40
- premultiplyAlpha: 'none',
41
- });
42
- }
43
-
44
- // Rasterizes an ImageBitmap into a raw RGBA8 buffer, drawing into the caller's
45
- // shared canvas (resized per level) so one canvas serves every mipmap.
46
- function rasterize(canvas: OffscreenCanvas, bmp: ImageBitmap): Uint8Array {
47
- const ctx = canvas.getContext('2d', { colorSpace: 'srgb', willReadFrequently: true }) as
48
- | OffscreenCanvasRenderingContext2D
49
- | null;
50
- if (!ctx) {
51
- throw new Error('web-image: no 2d context for OffscreenCanvas');
52
- }
53
- canvas.width = bmp.width;
54
- canvas.height = bmp.height;
55
- ctx.drawImage(bmp, 0, 0);
56
- const img = ctx.getImageData(0, 0, bmp.width, bmp.height);
57
- return new Uint8Array(img.data.buffer, img.data.byteOffset, img.data.byteLength);
54
+ function isLayered(bytes: Uint8Array): boolean {
55
+ return bytes.length >= OUTER_HEADER_SIZE && bytes[3] === MAGIC_LAYERED;
58
56
  }
59
57
 
60
58
  /**
61
- * Decodes the embedded PNG/WebP blobs of a `.ctex` file with the browser's
62
- * `createImageBitmap` and rebuilds it as a raw RGBA8 (`DATA_FORMAT_IMAGE`)
63
- * container. Non-ctex bytes, streamable textures, and `DATA_FORMAT_IMAGE` /
64
- * `DATA_FORMAT_BASIS_UNIVERSAL` containers are returned unchanged. Throws if an
65
- * embedded blob can't be decoded or its dimensions don't match the expected
66
- * mipmap layout, so a would-be undecodable texture fails loudly at materialize
67
- * time. A no-op outside browsers (no `createImageBitmap`). Mip levels are
68
- * decoded concurrently; the returned container's pixel data is ordered largest
69
- * to smallest, matching the input blob table.
59
+ * Parses a container into its layers — one for `GST2` (`.ctex`), N for `GSTL`
60
+ * (`.ctexarray`/`.ccube`/`.ccubearray`), in order. Returns `null` for anything
61
+ * else: non-ctex bytes, a streamable container, or payloads that aren't
62
+ * PNG/WebP-embedded (raw / BASIS / VRAM containers pass through untouched), or
63
+ * layers of differing geometry. Throws on a truncated level table so a malformed
64
+ * file fails loudly rather than decoding garbage.
70
65
  */
71
- export async function decodeCtex(bytes: Uint8Array): Promise<Uint8Array> {
66
+ export function parseCtex(bytes: Uint8Array): CtexLayer[] | null {
72
67
  if (
73
- typeof createImageBitmap !== 'function' ||
74
- bytes.length < SUB_HEADER_OFFSET + SUB_HEADER_SIZE ||
75
- String.fromCharCode(...bytes.subarray(0, 4)) !== CTEX_MAGIC
68
+ bytes.length < OUTER_HEADER_SIZE + SUB_HEADER_SIZE ||
69
+ bytes[0] !== 0x47 || // 'G'
70
+ bytes[1] !== 0x53 || // 'S'
71
+ bytes[2] !== 0x54 || // 'T'
72
+ (bytes[3] !== 0x32 && bytes[3] !== MAGIC_LAYERED)
76
73
  ) {
77
- return bytes;
74
+ return null;
78
75
  }
79
76
  const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
80
77
  if (dv.getUint32(16, true) & FORMAT_BIT_STREAM) {
81
- return bytes; // streamable layout differs; leave it to the engine
78
+ return null; // streamable layout differs; leave it to the engine
82
79
  }
83
- const dataFormat = dv.getUint32(SUB_HEADER_OFFSET, true);
84
- if (dataFormat !== DATA_FORMAT_PNG && dataFormat !== DATA_FORMAT_WEBP) {
85
- return bytes; // raw and BASIS_UNIVERSAL containers pass through
80
+ const count = isLayered(bytes) ? dv.getUint32(8, true) : 1;
81
+ const layers: CtexLayer[] = [];
82
+ let offset = OUTER_HEADER_SIZE;
83
+ for (let i = 0; i < count; i++) {
84
+ if (offset + SUB_HEADER_SIZE > bytes.length) {
85
+ return null;
86
+ }
87
+ const dataFormat = dv.getUint32(offset, true);
88
+ if (dataFormat !== DATA_FORMAT_PNG && dataFormat !== DATA_FORMAT_WEBP) {
89
+ return null;
90
+ }
91
+ const width = dv.getUint16(offset + 4, true);
92
+ const height = dv.getUint16(offset + 6, true);
93
+ const mipmaps = dv.getUint32(offset + 8, true);
94
+ const levels: { offset: number; size: number }[] = [];
95
+ let p = offset + SUB_HEADER_SIZE;
96
+ for (let level = 0; level <= mipmaps; level++) {
97
+ if (p + 4 > bytes.length) {
98
+ throw new Error(`web-image: truncated ctex at mipmap ${level}`);
99
+ }
100
+ const size = dv.getUint32(p, true);
101
+ p += 4;
102
+ if (p + size > bytes.length) {
103
+ throw new Error(`web-image: truncated ctex blob at mipmap ${level}`);
104
+ }
105
+ levels.push({ offset: p, size });
106
+ p += size;
107
+ }
108
+ const first = layers[0];
109
+ if (first && (width !== first.width || height !== first.height || mipmaps !== first.mipmaps)) {
110
+ return null; // texture arrays need uniform layer geometry
111
+ }
112
+ layers.push({ dataFormat, width, height, mipmaps, levels });
113
+ offset = p;
86
114
  }
87
- const w = dv.getUint16(SUB_HEADER_OFFSET + 4, true);
88
- const h = dv.getUint16(SUB_HEADER_OFFSET + 6, true);
89
- const mipmaps = dv.getUint32(SUB_HEADER_OFFSET + 8, true);
90
- const type = dataFormat === DATA_FORMAT_WEBP ? 'image/webp' : 'image/png';
115
+ return count === 0 ? null : layers;
116
+ }
117
+
118
+ /** Dimensions of a mipmap level (`max(dim >> level, 1)`). */
119
+ export function mipLevelDimensions(width: number, height: number, level: number): [number, number] {
120
+ return [Math.max(width >> level, 1), Math.max(height >> level, 1)];
121
+ }
122
+
123
+ /** Number of levels in the full mip chain down to 1×1 (>= 1). */
124
+ export function mipChainLength(width: number, height: number): number {
125
+ let levels = 1;
126
+ while (width > 1 || height > 1) {
127
+ width = Math.max(1, width >> 1);
128
+ height = Math.max(1, height >> 1);
129
+ levels++;
130
+ }
131
+ return levels;
132
+ }
91
133
 
92
- // Phase 1: bounds-check the blob table so every level's byte range is known
93
- // before any decode starts (a malformed container fails fast, no decode).
94
- const mips: MipEntry[] = [];
95
- let ofs = BLOB_TABLE_OFFSET;
96
- for (let i = 0; i <= mipmaps; i++) {
97
- if (ofs + 4 > bytes.length) {
98
- throw new Error(`web-image: truncated ctex at mipmap ${i}`);
134
+ // Bytes of one `DATA_FORMAT_IMAGE` image body: sub-header + tightly packed
135
+ // levels. RGBA8 is 4 bytes/pixel; BC7/ASTC are 16-byte 4x4 blocks whose
136
+ // dimensions round up, matching `Image::_get_dst_image_size`.
137
+ function bodySize(width: number, height: number, mipmaps: number, format: number): number {
138
+ const rgba8 = format === FORMAT_RGBA8;
139
+ const block = rgba8 ? 1 : 4;
140
+ const bytesPerBlock = rgba8 ? RGBA8_PIXEL_SIZE : 16;
141
+ let dataSize = 0;
142
+ for (let level = 0; level <= mipmaps; level++) {
143
+ const [w, h] = mipLevelDimensions(width, height, level);
144
+ dataSize += Math.ceil(w / block) * Math.ceil(h / block) * bytesPerBlock;
145
+ }
146
+ return SUB_HEADER_SIZE + dataSize;
147
+ }
148
+
149
+ /**
150
+ * Allocates a `.ctex` with `count` identical `DATA_FORMAT_IMAGE` bodies (one
151
+ * array, no intermediate copies), reporting `stride` — bytes per body, so layer
152
+ * `i`'s data goes at `BLOB_TABLE_OFFSET + i * stride`. The outer header is kept
153
+ * verbatim (the layer count / layered type are the container's, not ours),
154
+ * except that `GST2` also carries the image dimensions there, which are patched.
155
+ */
156
+ export function allocateCtex(
157
+ source: Uint8Array,
158
+ count: number,
159
+ width: number,
160
+ height: number,
161
+ mipmaps: number,
162
+ format: number,
163
+ ): { bytes: Uint8Array; stride: number } {
164
+ const stride = bodySize(width, height, mipmaps, format);
165
+ const bytes = new Uint8Array(OUTER_HEADER_SIZE + count * stride);
166
+ bytes.set(source.subarray(0, OUTER_HEADER_SIZE)); // outer header verbatim (version/flags)
167
+ const dv = new DataView(bytes.buffer);
168
+ if (!isLayered(source)) {
169
+ dv.setUint32(8, width, true); // GST2 outer width
170
+ dv.setUint32(12, height, true); // GST2 outer height
171
+ }
172
+ for (let i = 0; i < count; i++) {
173
+ const offset = OUTER_HEADER_SIZE + i * stride;
174
+ dv.setUint32(offset, DATA_FORMAT_IMAGE, true);
175
+ dv.setUint16(offset + 4, width, true);
176
+ dv.setUint16(offset + 6, height, true);
177
+ dv.setUint32(offset + 8, mipmaps, true);
178
+ dv.setUint32(offset + 12, format, true);
179
+ }
180
+ return { bytes, stride };
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Capability probing. Godot's Compatibility (GLES3) renderer is what the web
185
+ // wasm build uses, and it only uploads a compressed format natively when the
186
+ // matching GL extension is present; this build compiles in NO decompressors, so
187
+ // a format the device can't upload breaks the texture rather than degrading.
188
+ // It selects formats for layered/3D textures exactly as for 2D — the
189
+ // `astc_layered_supported`/`astc_3d_supported` flags it computes are only used
190
+ // to derive `astc_supported`/`astc_hdr_supported` — so this probe covers texture
191
+ // arrays and cubemaps too.
192
+ // ---------------------------------------------------------------------------
193
+
194
+ /**
195
+ * Compressed `Image::Format` targets for an RGBA texture, best first, filtered
196
+ * to what the running device can actually upload. A list because the encoder may
197
+ * also reject a format the browser exposes (e.g. spark skips BC on iOS); the
198
+ * caller walks it until one is encodable.
199
+ */
200
+ export function pickGodotFormats(): number[] {
201
+ let gl: WebGL2RenderingContext | null = null;
202
+ try {
203
+ // Prefer a DOM canvas (works everywhere WebGL2 does); fall back to
204
+ // OffscreenCanvas for worker contexts.
205
+ const canvas =
206
+ typeof document !== 'undefined'
207
+ ? document.createElement('canvas')
208
+ : typeof OffscreenCanvas !== 'undefined'
209
+ ? new OffscreenCanvas(1, 1)
210
+ : null;
211
+ gl = (canvas?.getContext('webgl2', {
212
+ antialias: false,
213
+ depth: false,
214
+ stencil: false,
215
+ alpha: false,
216
+ }) ?? null) as WebGL2RenderingContext | null;
217
+ } catch {
218
+ gl = null;
219
+ }
220
+ if (!gl) {
221
+ return [];
222
+ }
223
+ const formats: number[] = [];
224
+ try {
225
+ const exts = new Set(gl.getSupportedExtensions() ?? []);
226
+ if (exts.has('EXT_texture_compression_bptc')) {
227
+ formats.push(FORMAT_BPTC_RGBA);
99
228
  }
100
- const size = dv.getUint32(ofs, true);
101
- ofs += 4;
102
- if (ofs + size > bytes.length) {
103
- throw new Error(`web-image: truncated ctex blob at mipmap ${i}`);
229
+ if (exts.has('WEBGL_compressed_texture_astc')) {
230
+ formats.push(FORMAT_ASTC_4x4);
104
231
  }
105
- mips.push({ offset: ofs, size, width: Math.max(w >> i, 1), height: Math.max(h >> i, 1) });
106
- ofs += size;
232
+ } finally {
233
+ // Don't retain the probe context for the page's lifetime.
234
+ gl.getExtension('WEBGL_lose_context')?.loseContext();
107
235
  }
236
+ return formats;
237
+ }
238
+
239
+ /**
240
+ * Transforms fetched `.ctex` bytes into replacement bytes, or `null` to decline
241
+ * (staging then keeps the raw RGBA8 decode). Produced by `transcoder*.ts` and
242
+ * installed by `load.ts`'s staging.
243
+ */
244
+ export type CtexTranscoder = (bytes: Uint8Array) => Promise<Uint8Array | null>;
108
245
 
109
- // Phase 2: codec-decode all levels concurrently — the dominant cost now
110
- // overlaps instead of summing. Views (not copies) go into the Blob, which
111
- // materializes them once.
112
- // `decodeBitmap` feeds the Blob ctor, which needs an ArrayBuffer-backed view.
113
- const bitmaps = await Promise.all(
114
- mips.map((m) => decodeBitmap(bytes.subarray(m.offset, m.offset + m.size) as Uint8Array<ArrayBuffer>, type)),
246
+ /**
247
+ * Decodes the embedded PNG/WebP payloads of a `.ctex` with the browser's
248
+ * `createImageBitmap` and rebuilds it as raw RGBA8 (`DATA_FORMAT_IMAGE`) — for
249
+ * `GST2` and for each layer of a `GSTL` array/cubemap. Non-ctex bytes,
250
+ * streamable containers, and `DATA_FORMAT_IMAGE` / `DATA_FORMAT_BASIS_UNIVERSAL`
251
+ * containers are returned unchanged. Throws if an embedded payload can't be
252
+ * decoded or its dimensions don't match the expected mipmap layout, so a
253
+ * would-be undecodable texture fails loudly at materialize time. A no-op outside
254
+ * browsers (no `createImageBitmap`). Levels are decoded concurrently and written
255
+ * straight into the output container (no per-level array).
256
+ */
257
+ export async function decodeCtex(bytes: Uint8Array): Promise<Uint8Array> {
258
+ if (typeof createImageBitmap !== 'function') {
259
+ return bytes;
260
+ }
261
+ const layers = parseCtex(bytes);
262
+ if (!layers) {
263
+ return bytes;
264
+ }
265
+ const { width, height, mipmaps } = layers[0];
266
+ const { bytes: out, stride } = allocateCtex(bytes, layers.length, width, height, mipmaps, FORMAT_RGBA8);
267
+ for (let i = 0; i < layers.length; i++) {
268
+ await rasterizeLevels(bytes, layers[i], out, BLOB_TABLE_OFFSET + i * stride);
269
+ }
270
+ return out;
271
+ }
272
+
273
+ /**
274
+ * Decodes one PNG/WebP payload into an `ImageBitmap` (level 0 for an encoder,
275
+ * every level for {@link decodeCtex}). Pixel values are handed over unchanged
276
+ * (`colorSpaceConversion`/`premultiplyAlpha` off).
277
+ */
278
+ export function payloadBitmap(
279
+ bytes: Uint8Array,
280
+ dataFormat: number,
281
+ level: { offset: number; size: number },
282
+ ): Promise<ImageBitmap> {
283
+ const type = dataFormat === DATA_FORMAT_WEBP ? 'image/webp' : 'image/png';
284
+ // The Blob ctor needs an ArrayBuffer-backed view.
285
+ return createImageBitmap(
286
+ new Blob([bytes.subarray(level.offset, level.offset + level.size) as Uint8Array<ArrayBuffer>], { type }),
287
+ { colorSpaceConversion: 'none', premultiplyAlpha: 'none' },
115
288
  );
289
+ }
116
290
 
117
- // Phase 3: validate dimensions and rasterize serially into one shared
118
- // canvas; close() on an already-closed bitmap is a no-op, so a throw
119
- // mid-loop still releases every bitmap via the finally.
120
- const levels: Uint8Array[] = new Array(mips.length);
121
- const canvas = new OffscreenCanvas(w, h);
291
+ // Decodes a layer's PNG/WebP levels and rasterizes them into `out` at
292
+ // `outOffset`, reusing one canvas across levels.
293
+ async function rasterizeLevels(bytes: Uint8Array, layer: CtexLayer, out: Uint8Array, outOffset: number): Promise<void> {
294
+ const { width, height } = layer;
295
+ const bitmaps = await Promise.all(layer.levels.map((l) => payloadBitmap(bytes, layer.dataFormat, l)));
296
+ const canvas = new OffscreenCanvas(width, height);
122
297
  try {
123
- for (let i = 0; i < mips.length; i++) {
298
+ let p = outOffset;
299
+ for (let i = 0; i < bitmaps.length; i++) {
300
+ const [lw, lh] = mipLevelDimensions(width, height, i);
124
301
  const bmp = bitmaps[i];
125
- if (bmp.width !== mips[i].width || bmp.height !== mips[i].height) {
126
- throw new Error(
127
- `web-image: mipmap ${i} decoded as ${bmp.width}x${bmp.height}, expected ${mips[i].width}x${mips[i].height}`,
128
- );
302
+ if (bmp.width !== lw || bmp.height !== lh) {
303
+ throw new Error(`web-image: mipmap ${i} decoded as ${bmp.width}x${bmp.height}, expected ${lw}x${lh}`);
129
304
  }
130
- levels[i] = rasterize(canvas, bmp);
305
+ out.set(rasterize(canvas, bmp), p);
306
+ p += lw * lh * RGBA8_PIXEL_SIZE;
131
307
  }
132
308
  } finally {
309
+ // close() on an already-closed bitmap is a no-op.
133
310
  for (const bmp of bitmaps) {
134
311
  bmp.close();
135
312
  }
136
313
  }
314
+ }
137
315
 
138
- const dataSize = levels.reduce((n, level) => n + level.length, 0);
139
- const out = new Uint8Array(BLOB_TABLE_OFFSET + dataSize);
140
- out.set(bytes.subarray(0, OUTER_HEADER_SIZE)); // outer header verbatim
141
- const odv = new DataView(out.buffer);
142
- odv.setUint32(SUB_HEADER_OFFSET, DATA_FORMAT_IMAGE, true);
143
- odv.setUint16(SUB_HEADER_OFFSET + 4, w, true);
144
- odv.setUint16(SUB_HEADER_OFFSET + 6, h, true);
145
- odv.setUint32(SUB_HEADER_OFFSET + 8, mipmaps, true);
146
- odv.setUint32(SUB_HEADER_OFFSET + 12, FORMAT_RGBA8, true);
147
- let p = BLOB_TABLE_OFFSET;
148
- for (const level of levels) {
149
- out.set(level, p);
150
- p += level.length;
316
+ // Rasterizes an ImageBitmap into a raw RGBA8 view, drawing into the caller's
317
+ // shared canvas (resized per level) so one canvas serves every mipmap.
318
+ function rasterize(canvas: OffscreenCanvas, bmp: ImageBitmap): Uint8Array {
319
+ const ctx = canvas.getContext('2d', { colorSpace: 'srgb', willReadFrequently: true }) as
320
+ | OffscreenCanvasRenderingContext2D
321
+ | null;
322
+ if (!ctx) {
323
+ throw new Error('web-image: no 2d context for OffscreenCanvas');
151
324
  }
152
- return out;
325
+ canvas.width = bmp.width;
326
+ canvas.height = bmp.height;
327
+ ctx.drawImage(bmp, 0, 0);
328
+ const img = ctx.getImageData(0, 0, bmp.width, bmp.height);
329
+ return new Uint8Array(img.data.buffer, img.data.byteOffset, img.data.byteLength);
153
330
  }