@ringozz/godot 4.7.1-8 → 4.7.1-9

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
@@ -1,118 +1,118 @@
1
- /**********************************************************************
2
- Copyright (c) Vladimir Davidovich. All rights reserved.
3
- ***********************************************************************/
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.
9
-
10
- const CTEX_MAGIC = 'GST2';
11
- const DATA_FORMAT_IMAGE = 0;
12
- const DATA_FORMAT_PNG = 1;
13
- const DATA_FORMAT_WEBP = 2;
14
- const FORMAT_RGBA8 = 5;
15
- const FORMAT_BIT_STREAM = 1 << 22;
16
-
17
- // Outer header: magic(4) version(4) w(4) h(4) df(4) mipmap_limit(4) reserved(12).
18
- const OUTER_HEADER_SIZE = 36;
19
- // Sub-header: data_format(4) w(2) h(2) mipmaps(4) format(4).
20
- const SUB_HEADER_OFFSET = OUTER_HEADER_SIZE;
21
- const SUB_HEADER_SIZE = 16;
22
- const BLOB_TABLE_OFFSET = SUB_HEADER_OFFSET + SUB_HEADER_SIZE;
23
-
24
- interface DecodedBitmap {
25
- width: number;
26
- height: number;
27
- data: Uint8Array;
28
- }
29
-
30
- async function decodeWebBitmap(blob: Uint8Array<ArrayBuffer>, type: string): Promise<DecodedBitmap> {
31
- const bmp = await createImageBitmap(new Blob([blob], { type }), {
32
- imageOrientation: 'none',
33
- colorSpaceConversion: 'none',
34
- premultiplyAlpha: 'none',
35
- });
36
- try {
37
- const canvas = new OffscreenCanvas(bmp.width, bmp.height);
38
- const ctx = canvas.getContext('2d', { colorSpace: 'srgb', willReadFrequently: true }) as
39
- | OffscreenCanvasRenderingContext2D
40
- | null;
41
- if (!ctx) {
42
- throw new Error('web-image: no 2d context for OffscreenCanvas');
43
- }
44
- ctx.drawImage(bmp, 0, 0);
45
- const img = ctx.getImageData(0, 0, bmp.width, bmp.height);
46
- return { width: bmp.width, height: bmp.height, data: new Uint8Array(img.data.buffer, img.data.byteOffset, img.data.byteLength) };
47
- } finally {
48
- bmp.close();
49
- }
50
- }
51
-
52
- /**
53
- * Decodes the embedded PNG/WebP blobs of a `.ctex` file with the browser's
54
- * `createImageBitmap` and rebuilds it as a raw RGBA8 (`DATA_FORMAT_IMAGE`)
55
- * container. Non-ctex bytes, streamable textures, and `DATA_FORMAT_IMAGE` /
56
- * `DATA_FORMAT_BASIS_UNIVERSAL` containers are returned unchanged. Throws if an
57
- * embedded blob can't be decoded or its dimensions don't match the expected
58
- * mipmap layout, so a would-be undecodable texture fails loudly at materialize
59
- * time. A no-op outside browsers (no `createImageBitmap`).
60
- */
61
- export async function decodeCtex(bytes: Uint8Array): Promise<Uint8Array> {
62
- if (
63
- typeof createImageBitmap !== 'function' ||
64
- bytes.length < SUB_HEADER_OFFSET + SUB_HEADER_SIZE ||
65
- String.fromCharCode(...bytes.subarray(0, 4)) !== CTEX_MAGIC
66
- ) {
67
- return bytes;
68
- }
69
- const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
70
- if (dv.getUint32(16, true) & FORMAT_BIT_STREAM) {
71
- return bytes; // streamable layout differs; leave it to the engine
72
- }
73
- const dataFormat = dv.getUint32(SUB_HEADER_OFFSET, true);
74
- if (dataFormat !== DATA_FORMAT_PNG && dataFormat !== DATA_FORMAT_WEBP) {
75
- return bytes; // raw and BASIS_UNIVERSAL containers pass through
76
- }
77
- const w = dv.getUint16(SUB_HEADER_OFFSET + 4, true);
78
- const h = dv.getUint16(SUB_HEADER_OFFSET + 6, true);
79
- const mipmaps = dv.getUint32(SUB_HEADER_OFFSET + 8, true);
80
- const type = dataFormat === DATA_FORMAT_WEBP ? 'image/webp' : 'image/png';
81
-
82
- let ofs = BLOB_TABLE_OFFSET;
83
- const levels: Uint8Array[] = [];
84
- for (let i = 0; i <= mipmaps; i++) {
85
- if (ofs + 4 > bytes.length) {
86
- throw new Error(`web-image: truncated ctex at mipmap ${i}`);
87
- }
88
- const size = dv.getUint32(ofs, true);
89
- ofs += 4;
90
- if (ofs + size > bytes.length) {
91
- throw new Error(`web-image: truncated ctex blob at mipmap ${i}`);
92
- }
93
- const bmp = await decodeWebBitmap(bytes.slice(ofs, ofs + size), type);
94
- ofs += size;
95
- const ew = Math.max(w >> i, 1);
96
- const eh = Math.max(h >> i, 1);
97
- if (bmp.width !== ew || bmp.height !== eh) {
98
- throw new Error(`web-image: mipmap ${i} decoded as ${bmp.width}x${bmp.height}, expected ${ew}x${eh}`);
99
- }
100
- levels.push(bmp.data);
101
- }
102
-
103
- const dataSize = levels.reduce((n, level) => n + level.length, 0);
104
- const out = new Uint8Array(BLOB_TABLE_OFFSET + dataSize);
105
- out.set(bytes.subarray(0, OUTER_HEADER_SIZE)); // outer header verbatim
106
- const odv = new DataView(out.buffer);
107
- odv.setUint32(SUB_HEADER_OFFSET, DATA_FORMAT_IMAGE, true);
108
- odv.setUint16(SUB_HEADER_OFFSET + 4, w, true);
109
- odv.setUint16(SUB_HEADER_OFFSET + 6, h, true);
110
- odv.setUint32(SUB_HEADER_OFFSET + 8, mipmaps, true);
111
- odv.setUint32(SUB_HEADER_OFFSET + 12, FORMAT_RGBA8, true);
112
- let p = BLOB_TABLE_OFFSET;
113
- for (const level of levels) {
114
- out.set(level, p);
115
- p += level.length;
116
- }
117
- return out;
118
- }
1
+ /**********************************************************************
2
+ Copyright (c) Vladimir Davidovich. All rights reserved.
3
+ ***********************************************************************/
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.
9
+
10
+ const CTEX_MAGIC = 'GST2';
11
+ const DATA_FORMAT_IMAGE = 0;
12
+ const DATA_FORMAT_PNG = 1;
13
+ const DATA_FORMAT_WEBP = 2;
14
+ const FORMAT_RGBA8 = 5;
15
+ const FORMAT_BIT_STREAM = 1 << 22;
16
+
17
+ // Outer header: magic(4) version(4) w(4) h(4) df(4) mipmap_limit(4) reserved(12).
18
+ const OUTER_HEADER_SIZE = 36;
19
+ // Sub-header: data_format(4) w(2) h(2) mipmaps(4) format(4).
20
+ const SUB_HEADER_OFFSET = OUTER_HEADER_SIZE;
21
+ const SUB_HEADER_SIZE = 16;
22
+ const BLOB_TABLE_OFFSET = SUB_HEADER_OFFSET + SUB_HEADER_SIZE;
23
+
24
+ interface DecodedBitmap {
25
+ width: number;
26
+ height: number;
27
+ data: Uint8Array;
28
+ }
29
+
30
+ async function decodeWebBitmap(blob: Uint8Array<ArrayBuffer>, type: string): Promise<DecodedBitmap> {
31
+ const bmp = await createImageBitmap(new Blob([blob], { type }), {
32
+ imageOrientation: 'none',
33
+ colorSpaceConversion: 'none',
34
+ premultiplyAlpha: 'none',
35
+ });
36
+ try {
37
+ const canvas = new OffscreenCanvas(bmp.width, bmp.height);
38
+ const ctx = canvas.getContext('2d', { colorSpace: 'srgb', willReadFrequently: true }) as
39
+ | OffscreenCanvasRenderingContext2D
40
+ | null;
41
+ if (!ctx) {
42
+ throw new Error('web-image: no 2d context for OffscreenCanvas');
43
+ }
44
+ ctx.drawImage(bmp, 0, 0);
45
+ const img = ctx.getImageData(0, 0, bmp.width, bmp.height);
46
+ return { width: bmp.width, height: bmp.height, data: new Uint8Array(img.data.buffer, img.data.byteOffset, img.data.byteLength) };
47
+ } finally {
48
+ bmp.close();
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Decodes the embedded PNG/WebP blobs of a `.ctex` file with the browser's
54
+ * `createImageBitmap` and rebuilds it as a raw RGBA8 (`DATA_FORMAT_IMAGE`)
55
+ * container. Non-ctex bytes, streamable textures, and `DATA_FORMAT_IMAGE` /
56
+ * `DATA_FORMAT_BASIS_UNIVERSAL` containers are returned unchanged. Throws if an
57
+ * embedded blob can't be decoded or its dimensions don't match the expected
58
+ * mipmap layout, so a would-be undecodable texture fails loudly at materialize
59
+ * time. A no-op outside browsers (no `createImageBitmap`).
60
+ */
61
+ export async function decodeCtex(bytes: Uint8Array): Promise<Uint8Array> {
62
+ if (
63
+ typeof createImageBitmap !== 'function' ||
64
+ bytes.length < SUB_HEADER_OFFSET + SUB_HEADER_SIZE ||
65
+ String.fromCharCode(...bytes.subarray(0, 4)) !== CTEX_MAGIC
66
+ ) {
67
+ return bytes;
68
+ }
69
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
70
+ if (dv.getUint32(16, true) & FORMAT_BIT_STREAM) {
71
+ return bytes; // streamable layout differs; leave it to the engine
72
+ }
73
+ const dataFormat = dv.getUint32(SUB_HEADER_OFFSET, true);
74
+ if (dataFormat !== DATA_FORMAT_PNG && dataFormat !== DATA_FORMAT_WEBP) {
75
+ return bytes; // raw and BASIS_UNIVERSAL containers pass through
76
+ }
77
+ const w = dv.getUint16(SUB_HEADER_OFFSET + 4, true);
78
+ const h = dv.getUint16(SUB_HEADER_OFFSET + 6, true);
79
+ const mipmaps = dv.getUint32(SUB_HEADER_OFFSET + 8, true);
80
+ const type = dataFormat === DATA_FORMAT_WEBP ? 'image/webp' : 'image/png';
81
+
82
+ let ofs = BLOB_TABLE_OFFSET;
83
+ const levels: Uint8Array[] = [];
84
+ for (let i = 0; i <= mipmaps; i++) {
85
+ if (ofs + 4 > bytes.length) {
86
+ throw new Error(`web-image: truncated ctex at mipmap ${i}`);
87
+ }
88
+ const size = dv.getUint32(ofs, true);
89
+ ofs += 4;
90
+ if (ofs + size > bytes.length) {
91
+ throw new Error(`web-image: truncated ctex blob at mipmap ${i}`);
92
+ }
93
+ const bmp = await decodeWebBitmap(bytes.slice(ofs, ofs + size), type);
94
+ ofs += size;
95
+ const ew = Math.max(w >> i, 1);
96
+ const eh = Math.max(h >> i, 1);
97
+ if (bmp.width !== ew || bmp.height !== eh) {
98
+ throw new Error(`web-image: mipmap ${i} decoded as ${bmp.width}x${bmp.height}, expected ${ew}x${eh}`);
99
+ }
100
+ levels.push(bmp.data);
101
+ }
102
+
103
+ const dataSize = levels.reduce((n, level) => n + level.length, 0);
104
+ const out = new Uint8Array(BLOB_TABLE_OFFSET + dataSize);
105
+ out.set(bytes.subarray(0, OUTER_HEADER_SIZE)); // outer header verbatim
106
+ const odv = new DataView(out.buffer);
107
+ odv.setUint32(SUB_HEADER_OFFSET, DATA_FORMAT_IMAGE, true);
108
+ odv.setUint16(SUB_HEADER_OFFSET + 4, w, true);
109
+ odv.setUint16(SUB_HEADER_OFFSET + 6, h, true);
110
+ odv.setUint32(SUB_HEADER_OFFSET + 8, mipmaps, true);
111
+ odv.setUint32(SUB_HEADER_OFFSET + 12, FORMAT_RGBA8, true);
112
+ let p = BLOB_TABLE_OFFSET;
113
+ for (const level of levels) {
114
+ out.set(level, p);
115
+ p += level.length;
116
+ }
117
+ return out;
118
+ }