@forgeax/engine-image 0.1.4 → 0.1.7
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/README.md +5 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/decode-image-from-file.d.ts.map +1 -1
- package/dist/decode-image-from-file.mjs +5 -135
- package/dist/decode-image-from-file.mjs.map +1 -1
- package/dist/hdr-decoder.mjs +1 -1
- package/dist/hdr-decoder.mjs.map +1 -1
- package/dist/image-importer.d.ts.map +1 -1
- package/dist/image-importer.mjs +20 -100
- package/dist/image-importer.mjs.map +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +2 -36
- package/dist/index.mjs.map +1 -1
- package/dist/ktx2-encode.d.ts +2 -5
- package/dist/ktx2-encode.d.ts.map +1 -1
- package/dist/ktx2-encode.mjs +15 -17
- package/dist/ktx2-encode.mjs.map +1 -1
- package/dist/parse-image.d.ts +0 -1
- package/dist/parse-image.d.ts.map +1 -1
- package/dist/parse-image.mjs +3 -80
- package/dist/parse-image.mjs.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/image.unit.test.ts +28 -162
- package/src/decode-image-from-file.ts +3 -68
- package/src/errors.ts +1 -1
- package/src/image-decoder-browser.ts +1 -1
- package/src/image-importer.ts +3 -4
- package/src/index.ts +3 -5
- package/src/ktx2-encode.ts +17 -21
- package/src/parse-image.ts +3 -103
- package/dist/__tests__/ktx2-encode-mode-owner.test-d.d.ts +0 -2
- package/dist/__tests__/ktx2-encode-mode-owner.test-d.d.ts.map +0 -1
- package/dist/__tests__/runtime-decode.unit.test.d.ts +0 -2
- package/dist/__tests__/runtime-decode.unit.test.d.ts.map +0 -1
- package/dist/__tests__/tga.unit.test.d.ts +0 -2
- package/dist/__tests__/tga.unit.test.d.ts.map +0 -1
- package/dist/runtime/__tests__/asset-decoders.unit.test.d.ts +0 -2
- package/dist/runtime/__tests__/asset-decoders.unit.test.d.ts.map +0 -1
- package/dist/runtime/decode-image-bytes.d.ts +0 -7
- package/dist/runtime/decode-image-bytes.d.ts.map +0 -1
- package/dist/runtime/image-error.d.ts +0 -3
- package/dist/runtime/image-error.d.ts.map +0 -1
- package/src/__tests__/ktx2-encode-mode-owner.test-d.ts +0 -70
- package/src/__tests__/runtime-decode.unit.test.ts +0 -22
- package/src/__tests__/tga.unit.test.ts +0 -59
- package/src/runtime/__tests__/asset-decoders.unit.test.ts +0 -66
- package/src/runtime/decode-image-bytes.ts +0 -41
- package/src/runtime/image-error.ts +0 -8
package/src/parse-image.ts
CHANGED
|
@@ -45,7 +45,7 @@ export interface ParseImageOptions {
|
|
|
45
45
|
|
|
46
46
|
const DEFAULT_MAX_DIMENSION = 16384;
|
|
47
47
|
|
|
48
|
-
const SUPPORTED_MIMES: readonly string[] = ['image/png', 'image/jpeg'
|
|
48
|
+
const SUPPORTED_MIMES: readonly string[] = ['image/png', 'image/jpeg'];
|
|
49
49
|
|
|
50
50
|
interface UpngDecoded {
|
|
51
51
|
readonly width: number;
|
|
@@ -69,7 +69,6 @@ interface JpegDecoded {
|
|
|
69
69
|
* Decoder selection follows the mime hint:
|
|
70
70
|
* - `image/png` -> upng-js `UPNG.decode` + `UPNG.toRGBA8`
|
|
71
71
|
* - `image/jpeg` -> jpeg-js `decode` with `formatAsRGBA: true`
|
|
72
|
-
* - `image/x-tga` -> the built-in TGA true-color/grayscale decoder
|
|
73
72
|
* - other -> `image-format-unsupported`
|
|
74
73
|
*
|
|
75
74
|
* Decoded bytes are always tight-packed RGBA (`bytes.length === width *
|
|
@@ -119,7 +118,7 @@ export function parseImage(
|
|
|
119
118
|
);
|
|
120
119
|
}
|
|
121
120
|
rgba = new Uint8Array(first);
|
|
122
|
-
} else
|
|
121
|
+
} else {
|
|
123
122
|
// image/jpeg
|
|
124
123
|
const jpegMod: {
|
|
125
124
|
decode: (
|
|
@@ -136,11 +135,6 @@ export function parseImage(
|
|
|
136
135
|
width = decoded.width;
|
|
137
136
|
height = decoded.height;
|
|
138
137
|
rgba = decoded.data;
|
|
139
|
-
} else {
|
|
140
|
-
const decoded = decodeTga(bytes);
|
|
141
|
-
width = decoded.width;
|
|
142
|
-
height = decoded.height;
|
|
143
|
-
rgba = decoded.bytes;
|
|
144
138
|
}
|
|
145
139
|
} catch (e) {
|
|
146
140
|
return err(
|
|
@@ -175,102 +169,8 @@ export function parseImage(
|
|
|
175
169
|
bytes: rgba,
|
|
176
170
|
width,
|
|
177
171
|
height,
|
|
178
|
-
mime: mime as 'image/png' | 'image/jpeg'
|
|
172
|
+
mime: mime as 'image/png' | 'image/jpeg',
|
|
179
173
|
colorSpace: opts.colorSpace ?? 'srgb',
|
|
180
174
|
mipmap: opts.mipmap ?? true,
|
|
181
175
|
});
|
|
182
176
|
}
|
|
183
|
-
|
|
184
|
-
interface TgaDecoded {
|
|
185
|
-
readonly width: number;
|
|
186
|
-
readonly height: number;
|
|
187
|
-
readonly bytes: Uint8Array;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Decode the portable TGA subset used by common FBX exports.
|
|
192
|
-
*
|
|
193
|
-
* The decoder owns the format boundary instead of trusting the extension: it
|
|
194
|
-
* validates the header, supports uncompressed and RLE true-colour (24/32-bit)
|
|
195
|
-
* plus grayscale (8-bit), and applies the origin bits from the image
|
|
196
|
-
* descriptor while producing a top-left RGBA POD.
|
|
197
|
-
*/
|
|
198
|
-
function decodeTga(bytes: Uint8Array): TgaDecoded {
|
|
199
|
-
if (bytes.length < 18) throw new Error('TGA header is truncated');
|
|
200
|
-
|
|
201
|
-
const colorMapType = bytes[1];
|
|
202
|
-
const imageType = bytes[2];
|
|
203
|
-
const idLength = bytes[0] ?? 0;
|
|
204
|
-
const width = (bytes[12] ?? 0) | ((bytes[13] ?? 0) << 8);
|
|
205
|
-
const height = (bytes[14] ?? 0) | ((bytes[15] ?? 0) << 8);
|
|
206
|
-
const pixelDepth = bytes[16] ?? 0;
|
|
207
|
-
const descriptor = bytes[17] ?? 0;
|
|
208
|
-
if (colorMapType !== 0) throw new Error('color-mapped TGA is unsupported');
|
|
209
|
-
if (width < 1 || height < 1) throw new Error('TGA dimensions must be positive');
|
|
210
|
-
|
|
211
|
-
const isTrueColor = imageType === 2 || imageType === 10;
|
|
212
|
-
const isGray = imageType === 3 || imageType === 11;
|
|
213
|
-
if (!isTrueColor && !isGray) throw new Error(`TGA image type ${imageType} is unsupported`);
|
|
214
|
-
if (isTrueColor && pixelDepth !== 24 && pixelDepth !== 32) {
|
|
215
|
-
throw new Error(`TGA true-color depth ${pixelDepth} is unsupported`);
|
|
216
|
-
}
|
|
217
|
-
if (isGray && pixelDepth !== 8)
|
|
218
|
-
throw new Error(`TGA grayscale depth ${pixelDepth} is unsupported`);
|
|
219
|
-
|
|
220
|
-
const bytesPerPixel = pixelDepth / 8;
|
|
221
|
-
let offset = 18 + idLength;
|
|
222
|
-
const pixelCount = width * height;
|
|
223
|
-
const out = new Uint8Array(pixelCount * 4);
|
|
224
|
-
const topOrigin = (descriptor & 0x20) !== 0;
|
|
225
|
-
const rightOrigin = (descriptor & 0x10) !== 0;
|
|
226
|
-
let filePixel = 0;
|
|
227
|
-
|
|
228
|
-
const writePixel = (pixel: Uint8Array): void => {
|
|
229
|
-
if (filePixel >= pixelCount) throw new Error('TGA pixel data has too many pixels');
|
|
230
|
-
const fileX = filePixel % width;
|
|
231
|
-
const fileY = Math.floor(filePixel / width);
|
|
232
|
-
const x = rightOrigin ? width - 1 - fileX : fileX;
|
|
233
|
-
const y = topOrigin ? fileY : height - 1 - fileY;
|
|
234
|
-
const destination = (y * width + x) * 4;
|
|
235
|
-
if (isGray) {
|
|
236
|
-
const value = pixel[0] ?? 0;
|
|
237
|
-
out[destination] = value;
|
|
238
|
-
out[destination + 1] = value;
|
|
239
|
-
out[destination + 2] = value;
|
|
240
|
-
out[destination + 3] = 255;
|
|
241
|
-
} else {
|
|
242
|
-
out[destination] = pixel[2] ?? 0;
|
|
243
|
-
out[destination + 1] = pixel[1] ?? 0;
|
|
244
|
-
out[destination + 2] = pixel[0] ?? 0;
|
|
245
|
-
out[destination + 3] = bytesPerPixel === 4 ? (pixel[3] ?? 255) : 255;
|
|
246
|
-
}
|
|
247
|
-
filePixel += 1;
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
const readPixel = (): Uint8Array => {
|
|
251
|
-
if (offset + bytesPerPixel > bytes.length) throw new Error('TGA pixel data is truncated');
|
|
252
|
-
const pixel = bytes.slice(offset, offset + bytesPerPixel);
|
|
253
|
-
offset += bytesPerPixel;
|
|
254
|
-
return pixel;
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
if (imageType === 2 || imageType === 3) {
|
|
258
|
-
while (filePixel < pixelCount) writePixel(readPixel());
|
|
259
|
-
} else {
|
|
260
|
-
while (filePixel < pixelCount) {
|
|
261
|
-
if (offset >= bytes.length) throw new Error('TGA RLE packet header is truncated');
|
|
262
|
-
const packet = bytes[offset++] ?? 0;
|
|
263
|
-
const count = (packet & 0x7f) + 1;
|
|
264
|
-
if (filePixel + count > pixelCount) throw new Error('TGA RLE packet exceeds image bounds');
|
|
265
|
-
if ((packet & 0x80) !== 0) {
|
|
266
|
-
const pixel = readPixel();
|
|
267
|
-
for (let i = 0; i < count; i++) writePixel(pixel);
|
|
268
|
-
} else {
|
|
269
|
-
for (let i = 0; i < count; i++) writePixel(readPixel());
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (filePixel !== pixelCount) throw new Error('TGA pixel count mismatch');
|
|
275
|
-
return { width, height, bytes: out };
|
|
276
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ktx2-encode-mode-owner.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/ktx2-encode-mode-owner.test-d.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-decode.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/runtime-decode.unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tga.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/tga.unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"asset-decoders.unit.test.d.ts","sourceRoot":"","sources":["../../../src/runtime/__tests__/asset-decoders.unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { ImageError, Result, TextureAsset } from '@forgeax/engine-types';
|
|
2
|
-
/** Image-owned runtime boundary from encoded bytes to a TextureAsset POD. */
|
|
3
|
-
export declare function decodeImageBytes(bytes: Uint8Array | ArrayBuffer, mime: string, opts?: {
|
|
4
|
-
readonly colorSpace?: 'srgb' | 'linear';
|
|
5
|
-
readonly mipmap?: boolean;
|
|
6
|
-
}): Promise<Result<TextureAsset, ImageError>>;
|
|
7
|
-
//# sourceMappingURL=decode-image-bytes.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"decode-image-bytes.d.ts","sourceRoot":"","sources":["../../src/runtime/decode-image-bytes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAgB9E,6EAA6E;AAC7E,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,UAAU,GAAG,WAAW,EAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GAChF,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC,CAmB3C"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"image-error.d.ts","sourceRoot":"","sources":["../../src/runtime/image-error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAGhG,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,cAAc,EACxD,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC7B,aAAa,CAAC,CAAC,CAAC,CAElB"}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import type { BasisEncodeMode } from '@forgeax/engine-codec/encode';
|
|
5
|
-
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
6
|
-
import type {
|
|
7
|
-
BasisEncodeParams,
|
|
8
|
-
CompressionMode,
|
|
9
|
-
EncodeTextureResult,
|
|
10
|
-
ResolvedEncodeMode,
|
|
11
|
-
} from '../ktx2-encode.js';
|
|
12
|
-
|
|
13
|
-
type ExpectedCompressionMode = 'auto' | 'etc1s' | 'uastc' | 'none';
|
|
14
|
-
type ExpectedResolvedEncodeMode = 'etc1s' | 'uastc' | 'uastc-hdr' | 'none';
|
|
15
|
-
type EncodedMode = Extract<EncodeTextureResult, { readonly ok: true }>['value']['mode'];
|
|
16
|
-
type FailedMode = Extract<EncodeTextureResult, { readonly ok: false }>['error']['mode'];
|
|
17
|
-
|
|
18
|
-
const encodeSource = readFileSync(new URL('../ktx2-encode.ts', import.meta.url), 'utf8');
|
|
19
|
-
|
|
20
|
-
describe('image KTX2 encode mode owner', () => {
|
|
21
|
-
it('keeps both exact image-side vocabularies and their public projections', () => {
|
|
22
|
-
expectTypeOf<CompressionMode>().toEqualTypeOf<ExpectedCompressionMode>();
|
|
23
|
-
expectTypeOf<ExpectedCompressionMode>().toEqualTypeOf<CompressionMode>();
|
|
24
|
-
expectTypeOf<ResolvedEncodeMode>().toEqualTypeOf<ExpectedResolvedEncodeMode>();
|
|
25
|
-
expectTypeOf<ExpectedResolvedEncodeMode>().toEqualTypeOf<ResolvedEncodeMode>();
|
|
26
|
-
|
|
27
|
-
const acceptsCompressionMode = (mode: CompressionMode): CompressionMode => mode;
|
|
28
|
-
acceptsCompressionMode('auto');
|
|
29
|
-
acceptsCompressionMode('etc1s');
|
|
30
|
-
acceptsCompressionMode('uastc');
|
|
31
|
-
acceptsCompressionMode('none');
|
|
32
|
-
// @ts-expect-error Delivery modes are not sidecar control-plane values.
|
|
33
|
-
acceptsCompressionMode('uastc-hdr');
|
|
34
|
-
|
|
35
|
-
const acceptsResolvedMode = (mode: ResolvedEncodeMode): ResolvedEncodeMode => mode;
|
|
36
|
-
acceptsResolvedMode('etc1s');
|
|
37
|
-
acceptsResolvedMode('uastc');
|
|
38
|
-
acceptsResolvedMode('uastc-hdr');
|
|
39
|
-
acceptsResolvedMode('none');
|
|
40
|
-
// @ts-expect-error The control-plane-only auto mode is not a resolved encoding.
|
|
41
|
-
acceptsResolvedMode('auto');
|
|
42
|
-
|
|
43
|
-
expectTypeOf<EncodedMode>().toEqualTypeOf<ResolvedEncodeMode>();
|
|
44
|
-
expectTypeOf<FailedMode>().toEqualTypeOf<ResolvedEncodeMode>();
|
|
45
|
-
expectTypeOf<BasisEncodeParams['mode']>().toEqualTypeOf<BasisEncodeMode>();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('keeps the runtime tuples as the single image-side mode owners', () => {
|
|
49
|
-
expect(encodeSource).toContain(
|
|
50
|
-
"const COMPRESSION_MODES = ['auto', 'etc1s', 'uastc', 'none'] as const;",
|
|
51
|
-
);
|
|
52
|
-
expect(encodeSource).toContain(
|
|
53
|
-
'export type CompressionMode = (typeof COMPRESSION_MODES)[number];',
|
|
54
|
-
);
|
|
55
|
-
expect(encodeSource).toContain(
|
|
56
|
-
"const RESOLVED_ENCODE_MODES = ['etc1s', 'uastc', 'uastc-hdr', 'none'] as const;",
|
|
57
|
-
);
|
|
58
|
-
expect(encodeSource).toContain(
|
|
59
|
-
'export type ResolvedEncodeMode = (typeof RESOLVED_ENCODE_MODES)[number];',
|
|
60
|
-
);
|
|
61
|
-
expect(encodeSource).toContain('case COMPRESSION_MODES[3]:');
|
|
62
|
-
expect(encodeSource).toContain('case RESOLVED_ENCODE_MODES[2]:');
|
|
63
|
-
expect(encodeSource).not.toContain(
|
|
64
|
-
"export type CompressionMode = 'auto' | 'etc1s' | 'uastc' | 'none';",
|
|
65
|
-
);
|
|
66
|
-
expect(encodeSource).not.toContain(
|
|
67
|
-
"export type ResolvedEncodeMode = 'etc1s' | 'uastc' | 'uastc-hdr' | 'none';",
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { decodeImageBytes } from '../runtime/decode-image-bytes';
|
|
3
|
-
|
|
4
|
-
describe('image runtime decode owner', () => {
|
|
5
|
-
it('returns a closed failure when the host has no browser decoder', async () => {
|
|
6
|
-
const result = await decodeImageBytes(new Uint8Array([0x89, 0x50]), 'image/png');
|
|
7
|
-
|
|
8
|
-
expect(result.ok).toBe(false);
|
|
9
|
-
if (result.ok) return;
|
|
10
|
-
expect(result.error.code).toBe('image-decode-failed');
|
|
11
|
-
expect(result.error.expected).toContain('decodes successfully');
|
|
12
|
-
expect(result.error.hint).toBeTruthy();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('does not silently reinterpret a codec MIME as a different image kind', async () => {
|
|
16
|
-
const result = await decodeImageBytes(new Uint8Array([1, 2, 3]), 'image/jpeg');
|
|
17
|
-
|
|
18
|
-
expect(result.ok).toBe(false);
|
|
19
|
-
if (result.ok) return;
|
|
20
|
-
expect(result.error.detail.code).toBe('image-decode-failed');
|
|
21
|
-
});
|
|
22
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { parseImage } from '../parse-image.js';
|
|
3
|
-
|
|
4
|
-
function makeTga24(
|
|
5
|
-
width: number,
|
|
6
|
-
height: number,
|
|
7
|
-
bgrPixels: readonly number[],
|
|
8
|
-
imageDescriptor = 0,
|
|
9
|
-
): Uint8Array {
|
|
10
|
-
const header = new Uint8Array(18);
|
|
11
|
-
header[2] = 2; // uncompressed true-color
|
|
12
|
-
header[12] = width & 0xff;
|
|
13
|
-
header[13] = (width >>> 8) & 0xff;
|
|
14
|
-
header[14] = height & 0xff;
|
|
15
|
-
header[15] = (height >>> 8) & 0xff;
|
|
16
|
-
header[16] = 24;
|
|
17
|
-
header[17] = imageDescriptor;
|
|
18
|
-
return new Uint8Array([...header, ...bgrPixels]);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe('parseImage TGA', () => {
|
|
22
|
-
it('decodes 24-bit true-color pixels to RGBA and honours bottom-left origin', () => {
|
|
23
|
-
// File order is bottom row first: blue, green, then top row red, yellow.
|
|
24
|
-
// The descriptor has no top bit, so the decoded image must be top row
|
|
25
|
-
// first while preserving left-to-right order.
|
|
26
|
-
const tga = makeTga24(2, 2, [255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 255, 255]);
|
|
27
|
-
const result = parseImage(tga, 'image/x-tga', { mipmap: false });
|
|
28
|
-
|
|
29
|
-
expect(result.ok).toBe(true);
|
|
30
|
-
if (!result.ok) return;
|
|
31
|
-
expect(result.value.mime).toBe('image/x-tga');
|
|
32
|
-
expect(Array.from(result.value.bytes)).toEqual([
|
|
33
|
-
255, 0, 0, 255, 255, 255, 0, 255, 0, 0, 255, 255, 0, 255, 0, 255,
|
|
34
|
-
]);
|
|
35
|
-
expect(result.value.width).toBe(2);
|
|
36
|
-
expect(result.value.height).toBe(2);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('decodes 32-bit RLE pixels and preserves alpha', () => {
|
|
40
|
-
const header = new Uint8Array(18);
|
|
41
|
-
header[2] = 10; // RLE true-color
|
|
42
|
-
header[12] = 2;
|
|
43
|
-
header[14] = 1;
|
|
44
|
-
header[16] = 32;
|
|
45
|
-
const tga = new Uint8Array([
|
|
46
|
-
...header,
|
|
47
|
-
0x81, // run of two pixels
|
|
48
|
-
10,
|
|
49
|
-
20,
|
|
50
|
-
30,
|
|
51
|
-
40,
|
|
52
|
-
]);
|
|
53
|
-
const result = parseImage(tga, 'image/x-tga');
|
|
54
|
-
|
|
55
|
-
expect(result.ok).toBe(true);
|
|
56
|
-
if (!result.ok) return;
|
|
57
|
-
expect(Array.from(result.value.bytes)).toEqual([30, 20, 10, 40, 30, 20, 10, 40]);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { ok } from '@forgeax/engine-types';
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
vi.mock('@forgeax/engine-codec', () => ({
|
|
5
|
-
parseKtx2: vi.fn(async () => ({ ok: true, value: {} })),
|
|
6
|
-
transcodeBasis: vi.fn(),
|
|
7
|
-
transcodeKtx2: vi.fn(async (_parsed: unknown, target: string) => ({
|
|
8
|
-
ok: true,
|
|
9
|
-
value: {
|
|
10
|
-
format: target,
|
|
11
|
-
width: 2,
|
|
12
|
-
height: 2,
|
|
13
|
-
mips: [{ level: 0, width: 2, height: 2, data: new Uint8Array(16) }],
|
|
14
|
-
},
|
|
15
|
-
})),
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
import { textureContribution } from '../asset-decoders.js';
|
|
19
|
-
|
|
20
|
-
describe('runtime image asset decoder', () => {
|
|
21
|
-
it('transcodes Basis KTX2 artifacts to the uncompressed TextureAsset baseline', async () => {
|
|
22
|
-
const result = await textureContribution.decoder.decode({
|
|
23
|
-
envelope: {
|
|
24
|
-
guid: '019e3969-1d47-760f-982e-7bad1ffd969c',
|
|
25
|
-
kind: 'texture',
|
|
26
|
-
payload: {
|
|
27
|
-
kind: 'texture',
|
|
28
|
-
width: 4,
|
|
29
|
-
height: 4,
|
|
30
|
-
format: 'rgba8unorm-srgb',
|
|
31
|
-
colorSpace: 'srgb',
|
|
32
|
-
mipmap: true,
|
|
33
|
-
data: new Uint8Array(64),
|
|
34
|
-
},
|
|
35
|
-
refs: [],
|
|
36
|
-
artifacts: {
|
|
37
|
-
body: {
|
|
38
|
-
path: 'body.bin',
|
|
39
|
-
mediaType: 'image/ktx2',
|
|
40
|
-
assetCodec: { name: 'basis', container: 'ktx2', profile: 'etc1s' },
|
|
41
|
-
contentEncoding: 'identity',
|
|
42
|
-
byteLength: 4,
|
|
43
|
-
integrity: { algorithm: 'sha256', digest: `sha256:${'0'.repeat(64)}` },
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
artifacts: {
|
|
48
|
-
read: async () => ok(new Uint8Array([1, 2, 3, 4])),
|
|
49
|
-
},
|
|
50
|
-
signal: new AbortController().signal,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
expect(result).toEqual({
|
|
54
|
-
ok: true,
|
|
55
|
-
value: expect.objectContaining({
|
|
56
|
-
kind: 'texture',
|
|
57
|
-
width: 2,
|
|
58
|
-
height: 2,
|
|
59
|
-
format: 'rgba8unorm-srgb',
|
|
60
|
-
colorSpace: 'srgb',
|
|
61
|
-
mipmap: true,
|
|
62
|
-
data: expect.any(Uint8Array),
|
|
63
|
-
}),
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
});
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { ImageError, Result, TextureAsset } from '@forgeax/engine-types';
|
|
2
|
-
import { err, ok } from '@forgeax/engine-types';
|
|
3
|
-
import { decodeImageInBrowser } from '../image-decoder-browser';
|
|
4
|
-
import { runtimeImageError } from './image-error';
|
|
5
|
-
|
|
6
|
-
const SUPPORTED_MIMES = ['image/png', 'image/jpeg'] as const;
|
|
7
|
-
type SupportedMime = (typeof SUPPORTED_MIMES)[number];
|
|
8
|
-
|
|
9
|
-
function isSupportedMime(mime: string): mime is SupportedMime {
|
|
10
|
-
return SUPPORTED_MIMES.some((supportedMime) => supportedMime === mime);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function mipLevels(width: number, height: number): number {
|
|
14
|
-
return Math.floor(Math.log2(Math.max(width, height))) + 1;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Image-owned runtime boundary from encoded bytes to a TextureAsset POD. */
|
|
18
|
-
export async function decodeImageBytes(
|
|
19
|
-
bytes: Uint8Array | ArrayBuffer,
|
|
20
|
-
mime: string,
|
|
21
|
-
opts: { readonly colorSpace?: 'srgb' | 'linear'; readonly mipmap?: boolean } = {},
|
|
22
|
-
): Promise<Result<TextureAsset, ImageError>> {
|
|
23
|
-
if (!isSupportedMime(mime)) {
|
|
24
|
-
return err(runtimeImageError({ code: 'image-format-unsupported', actualMime: mime }));
|
|
25
|
-
}
|
|
26
|
-
const colorSpace = opts.colorSpace ?? 'srgb';
|
|
27
|
-
const mipmap = opts.mipmap ?? true;
|
|
28
|
-
const input = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
29
|
-
const decoded = await decodeImageInBrowser(input, mime as SupportedMime, { colorSpace, mipmap });
|
|
30
|
-
if (!decoded.ok) return err(decoded.error);
|
|
31
|
-
return ok({
|
|
32
|
-
kind: 'texture',
|
|
33
|
-
width: decoded.value.width,
|
|
34
|
-
height: decoded.value.height,
|
|
35
|
-
format: colorSpace === 'srgb' ? 'rgba8unorm-srgb' : 'rgba8unorm',
|
|
36
|
-
data: decoded.value.bytes,
|
|
37
|
-
colorSpace,
|
|
38
|
-
mipmap,
|
|
39
|
-
mipLevelCount: mipmap ? mipLevels(decoded.value.width, decoded.value.height) : 1,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { ImageErrorCode, ImageErrorDetailFor, ImageErrorFor } from '@forgeax/engine-types';
|
|
2
|
-
import { imageError } from '../errors';
|
|
3
|
-
|
|
4
|
-
export function runtimeImageError<C extends ImageErrorCode>(
|
|
5
|
-
detail: ImageErrorDetailFor<C>,
|
|
6
|
-
): ImageErrorFor<C> {
|
|
7
|
-
return imageError(detail);
|
|
8
|
-
}
|