@forgeax/engine-image 0.0.0-dev.8d955ade1c79

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.
Files changed (104) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +166 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/compression-mode.unit.test.d.ts +2 -0
  5. package/dist/__tests__/compression-mode.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/encode-source-too-large.unit.test.d.ts +2 -0
  7. package/dist/__tests__/encode-source-too-large.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/errors.test-d.d.ts +2 -0
  9. package/dist/__tests__/errors.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/image-importer-conversion-failure.unit.test.d.ts +2 -0
  11. package/dist/__tests__/image-importer-conversion-failure.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/image-importer-hdr-equirect.test.d.ts +2 -0
  13. package/dist/__tests__/image-importer-hdr-equirect.test.d.ts.map +1 -0
  14. package/dist/__tests__/image-importer-topology.unit.test.d.ts +2 -0
  15. package/dist/__tests__/image-importer-topology.unit.test.d.ts.map +1 -0
  16. package/dist/__tests__/image-importer.test.d.ts +2 -0
  17. package/dist/__tests__/image-importer.test.d.ts.map +1 -0
  18. package/dist/__tests__/image-local-artifacts.test.d.ts +2 -0
  19. package/dist/__tests__/image-local-artifacts.test.d.ts.map +1 -0
  20. package/dist/__tests__/image.unit.test.d.ts +2 -0
  21. package/dist/__tests__/image.unit.test.d.ts.map +1 -0
  22. package/dist/__tests__/ktx2-basis-importer.unit.test.d.ts +2 -0
  23. package/dist/__tests__/ktx2-basis-importer.unit.test.d.ts.map +1 -0
  24. package/dist/__tests__/make-fixture.d.ts +12 -0
  25. package/dist/__tests__/make-fixture.d.ts.map +1 -0
  26. package/dist/__tests__/parse-image-downscale.test.d.ts +2 -0
  27. package/dist/__tests__/parse-image-downscale.test.d.ts.map +1 -0
  28. package/dist/__tests__/source-key.unit.test.d.ts +2 -0
  29. package/dist/__tests__/source-key.unit.test.d.ts.map +1 -0
  30. package/dist/decode-image-from-file.d.ts +40 -0
  31. package/dist/decode-image-from-file.d.ts.map +1 -0
  32. package/dist/decode-image-from-file.mjs +460 -0
  33. package/dist/decode-image-from-file.mjs.map +1 -0
  34. package/dist/errors.d.ts +14 -0
  35. package/dist/errors.d.ts.map +1 -0
  36. package/dist/hdr-decoder.d.ts +28 -0
  37. package/dist/hdr-decoder.d.ts.map +1 -0
  38. package/dist/hdr-decoder.mjs +212 -0
  39. package/dist/hdr-decoder.mjs.map +1 -0
  40. package/dist/image-decoder-browser.d.ts +22 -0
  41. package/dist/image-decoder-browser.d.ts.map +1 -0
  42. package/dist/image-decoder-node.d.ts +50 -0
  43. package/dist/image-decoder-node.d.ts.map +1 -0
  44. package/dist/image-importer.d.ts +18 -0
  45. package/dist/image-importer.d.ts.map +1 -0
  46. package/dist/image-importer.mjs +1085 -0
  47. package/dist/image-importer.mjs.map +1 -0
  48. package/dist/index.d.ts +17 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.mjs +469 -0
  51. package/dist/index.mjs.map +1 -0
  52. package/dist/ktx2-encode.d.ts +81 -0
  53. package/dist/ktx2-encode.d.ts.map +1 -0
  54. package/dist/ktx2-encode.mjs +98 -0
  55. package/dist/ktx2-encode.mjs.map +1 -0
  56. package/dist/parse-image.d.ts +47 -0
  57. package/dist/parse-image.d.ts.map +1 -0
  58. package/dist/parse-image.mjs +154 -0
  59. package/dist/parse-image.mjs.map +1 -0
  60. package/dist/reimport-reuse-meta.d.ts +81 -0
  61. package/dist/reimport-reuse-meta.d.ts.map +1 -0
  62. package/dist/resize-image.d.ts +7 -0
  63. package/dist/resize-image.d.ts.map +1 -0
  64. package/dist/result.d.ts +2 -0
  65. package/dist/result.d.ts.map +1 -0
  66. package/dist/runtime/asset-decoders.d.ts +4 -0
  67. package/dist/runtime/asset-decoders.d.ts.map +1 -0
  68. package/dist/source-key.d.ts +21 -0
  69. package/dist/source-key.d.ts.map +1 -0
  70. package/dist/sub-asset-key.d.ts +50 -0
  71. package/dist/sub-asset-key.d.ts.map +1 -0
  72. package/dist/to-asset-pack.d.ts +61 -0
  73. package/dist/to-asset-pack.d.ts.map +1 -0
  74. package/package.json +105 -0
  75. package/src/__tests__/compression-mode.unit.test.ts +104 -0
  76. package/src/__tests__/encode-source-too-large.unit.test.ts +81 -0
  77. package/src/__tests__/errors.test-d.ts +148 -0
  78. package/src/__tests__/image-importer-conversion-failure.unit.test.ts +297 -0
  79. package/src/__tests__/image-importer-hdr-equirect.test.ts +218 -0
  80. package/src/__tests__/image-importer-topology.unit.test.ts +209 -0
  81. package/src/__tests__/image-importer.test.ts +12 -0
  82. package/src/__tests__/image-local-artifacts.test.ts +38 -0
  83. package/src/__tests__/image.unit.test.ts +1253 -0
  84. package/src/__tests__/ktx2-basis-importer.unit.test.ts +126 -0
  85. package/src/__tests__/make-fixture.ts +100 -0
  86. package/src/__tests__/parse-image-downscale.test.ts +26 -0
  87. package/src/__tests__/source-key.unit.test.ts +65 -0
  88. package/src/decode-image-from-file.ts +233 -0
  89. package/src/errors.ts +60 -0
  90. package/src/hdr-decoder.ts +255 -0
  91. package/src/image-decoder-browser.ts +88 -0
  92. package/src/image-decoder-node.ts +67 -0
  93. package/src/image-decoders.d.ts +23 -0
  94. package/src/image-importer.ts +858 -0
  95. package/src/index.ts +44 -0
  96. package/src/ktx2-encode.ts +217 -0
  97. package/src/parse-image.ts +176 -0
  98. package/src/reimport-reuse-meta.ts +160 -0
  99. package/src/resize-image.ts +39 -0
  100. package/src/result.ts +13 -0
  101. package/src/runtime/asset-decoders.ts +152 -0
  102. package/src/source-key.ts +41 -0
  103. package/src/sub-asset-key.ts +67 -0
  104. package/src/to-asset-pack.ts +95 -0
@@ -0,0 +1,1085 @@
1
+ import { err, ok, ImportError, IMPORT_ERROR_HINTS, IMAGE_ERROR_HINTS } from '@forgeax/engine-types';
2
+ import { parseKtx2, ktx2ColorSpace } from '@forgeax/engine-codec';
3
+ import { basisEncode } from '@forgeax/engine-codec/encode';
4
+ import * as jpeg from 'jpeg-js';
5
+ import * as UPNG from 'upng-js';
6
+
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ function imageError(detail) {
17
+ return new ImageErrorImpl(detail);
18
+ }
19
+ var IMAGE_ERROR_EXPECTED, ImageErrorImpl;
20
+ var init_errors = __esm({
21
+ "src/errors.ts"() {
22
+ IMAGE_ERROR_EXPECTED = {
23
+ "image-decode-failed": "PNG / JPG byte stream decodes successfully",
24
+ "image-format-unsupported": "mime is one of ['image/png', 'image/jpeg']; uploadTexture format <-> colorSpace family agrees",
25
+ "image-dimension-out-of-bounds": "width and height fall under device caps maxTextureDimension2D (or 16384 hard cap)",
26
+ "image-meta-missing": "<source>.meta.json sidecar (importer: 'image') exists in the same directory",
27
+ "image-hdr-decode-failed": "Radiance RGBE header is valid and pixel data decodes successfully",
28
+ // feat-20260521-sprite-atlas-animation M1 T-03 — vite-plugin-image atlas
29
+ // hook .expected literals (plan-strategy section 2 D-2 + AC-10 a/b/c).
30
+ // ImageErrorImpl construction path is unchanged: the new atlas-* errors
31
+ // flow through `new ImageErrorImpl({ code: 'atlas-...', ...detail })` and
32
+ // pick the .expected string up from this Record at construction time
33
+ // (charter P3 explicit failure SSOT — AI users surface .expected next to
34
+ // .hint after switch (err.code) without parsing the message).
35
+ "atlas-empty-input": "images.length >= 1",
36
+ "atlas-size-exceeded": "image width x height <= maxAtlasSize^2 and each image fits in the atlas footprint",
37
+ "atlas-region-mismatch": "sum(regions[i].w x regions[i].h) <= atlasWidth x atlasHeight"
38
+ };
39
+ ImageErrorImpl = class extends Error {
40
+ code;
41
+ expected;
42
+ hint;
43
+ detail;
44
+ constructor(detail) {
45
+ const code = detail.code;
46
+ const expected = IMAGE_ERROR_EXPECTED[code];
47
+ const hint = IMAGE_ERROR_HINTS[code];
48
+ super(`[ImageError ${code}] expected: ${expected}; hint: ${hint}`);
49
+ this.name = "ImageError";
50
+ this.code = code;
51
+ this.expected = expected;
52
+ this.hint = hint;
53
+ this.detail = detail;
54
+ }
55
+ };
56
+ }
57
+ });
58
+ var init_result = __esm({
59
+ "src/result.ts"() {
60
+ }
61
+ });
62
+
63
+ // src/hdr-decoder.ts
64
+ var hdr_decoder_exports = {};
65
+ __export(hdr_decoder_exports, {
66
+ decodeHdr: () => decodeHdr
67
+ });
68
+ function decodeHdr(bytes) {
69
+ let pos = 0;
70
+ if (bytes.length < 11) {
71
+ return err(hdrDecodeError("file too short for Radiance HDR header"));
72
+ }
73
+ const magic = String.fromCharCode(...bytes.subarray(0, 11));
74
+ if (magic !== "#?RADIANCE\n" && magic !== "#?RGBE\n") {
75
+ return err(
76
+ hdrDecodeError("missing or invalid Radiance HDR magic; expected #?RADIANCE or #?RGBE")
77
+ );
78
+ }
79
+ pos = 11;
80
+ let formatFound = false;
81
+ while (pos < bytes.length) {
82
+ const lineEnd = findByte(bytes, LF, pos);
83
+ if (lineEnd === -1) return err(hdrDecodeError("header truncated before empty line"));
84
+ const line = asciiSubstring(bytes, pos, lineEnd);
85
+ pos = lineEnd + 1;
86
+ if (line === "") break;
87
+ if (line.startsWith("FORMAT=")) {
88
+ const val = line.slice(7);
89
+ if (val === "32-bit_rle_rgbe") {
90
+ formatFound = true;
91
+ }
92
+ }
93
+ }
94
+ if (!formatFound) {
95
+ return err(hdrDecodeError("missing or unsupported FORMAT; expected FORMAT=32-bit_rle_rgbe"));
96
+ }
97
+ if (pos >= bytes.length) return err(hdrDecodeError("missing resolution line"));
98
+ const resLineEnd = findByte(bytes, LF, pos);
99
+ if (resLineEnd === -1) return err(hdrDecodeError("resolution line truncated"));
100
+ const resLine = asciiSubstring(bytes, pos, resLineEnd);
101
+ pos = resLineEnd + 1;
102
+ const resMatch = /^-Y\s+(\d+)\s+\+X\s+(\d+)\s*$/.exec(resLine);
103
+ if (resMatch === null) {
104
+ return err(
105
+ hdrDecodeError(`unexpected resolution line format: "${resLine}"; expected "-Y H +X W"`)
106
+ );
107
+ }
108
+ const height = Number(resMatch[1]);
109
+ const width = Number(resMatch[2]);
110
+ if (height === void 0 || width === void 0 || height <= 0 || width <= 0) {
111
+ return err(hdrDecodeError(`invalid dimensions: ${width}x${height}`));
112
+ }
113
+ const pixelCount = width * height;
114
+ const rgbe = new Uint8Array(pixelCount * 4);
115
+ const scanlineBytes = width * 4;
116
+ for (let y = 0; y < height; y++) {
117
+ if (pos + 4 > bytes.length) {
118
+ return err(hdrDecodeError(`truncated at scanline ${y}: expected 4-byte prefix`));
119
+ }
120
+ const p0 = bytes[pos];
121
+ const p1 = bytes[pos + 1];
122
+ const pHi = bytes[pos + 2];
123
+ const pLo = bytes[pos + 3];
124
+ if (p0 === void 0 || p1 === void 0 || pHi === void 0 || pLo === void 0) {
125
+ return err(hdrDecodeError(`truncated at scanline ${y}`));
126
+ }
127
+ const prefixWidth = pHi << 8 | pLo;
128
+ if (p0 === 2 && p1 === 2 && prefixWidth === width && width >= 8 && width <= 32767) {
129
+ const decoded = decodeNewRleScanline(bytes, pos + 4, width);
130
+ if (decoded === null) {
131
+ return err(hdrDecodeError(`RLE decode failed at scanline ${y}`));
132
+ }
133
+ const base = y * scanlineBytes;
134
+ const channels = [decoded[0], decoded[1], decoded[2], decoded[3]];
135
+ for (let x = 0; x < width; x++) {
136
+ for (let ch = 0; ch < 4; ch++) {
137
+ rgbe[base + x * 4 + ch] = channels[ch][x];
138
+ }
139
+ }
140
+ pos += 4 + decoded.totalBytes;
141
+ } else {
142
+ return err(
143
+ hdrDecodeError(
144
+ "old-RLE format is not supported; only new-RLE (32-bit_rle_rgbe) is accepted"
145
+ )
146
+ );
147
+ }
148
+ }
149
+ const out = new Float32Array(pixelCount * 4);
150
+ for (let i = 0; i < pixelCount; i++) {
151
+ const base = i * 4;
152
+ const r = rgbe[base];
153
+ const g = rgbe[base + 1];
154
+ const b = rgbe[base + 2];
155
+ const e = rgbe[base + 3];
156
+ out[base + 3] = 1;
157
+ if (e === 0) {
158
+ out[base] = 0;
159
+ out[base + 1] = 0;
160
+ out[base + 2] = 0;
161
+ } else {
162
+ const f = 2 ** (e - 136);
163
+ out[base] = (r + 0.5) * f;
164
+ out[base + 1] = (g + 0.5) * f;
165
+ out[base + 2] = (b + 0.5) * f;
166
+ }
167
+ }
168
+ return ok({ width, height, data: out });
169
+ }
170
+ function decodeNewRleScanline(bytes, start, width) {
171
+ const channels = [];
172
+ let cursor = start;
173
+ for (let ch = 0; ch < 4; ch++) {
174
+ const data = new Uint8Array(width);
175
+ let x = 0;
176
+ while (x < width) {
177
+ if (cursor >= bytes.length) return null;
178
+ const code = bytes[cursor];
179
+ cursor++;
180
+ if (code > 128) {
181
+ const count = code - 128;
182
+ if (cursor >= bytes.length || x + count > width) return null;
183
+ const val = bytes[cursor];
184
+ cursor++;
185
+ data.fill(val, x, x + count);
186
+ x += count;
187
+ } else {
188
+ if (code === 0) continue;
189
+ if (cursor + code > bytes.length || x + code > width) return null;
190
+ for (let k = 0; k < code; k++) {
191
+ data[x + k] = bytes[cursor + k];
192
+ }
193
+ cursor += code;
194
+ x += code;
195
+ }
196
+ }
197
+ channels.push(data);
198
+ }
199
+ const ch0 = channels[0];
200
+ const ch1 = channels[1];
201
+ const ch2 = channels[2];
202
+ const ch3 = channels[3];
203
+ if (ch0 === void 0 || ch1 === void 0 || ch2 === void 0 || ch3 === void 0) {
204
+ return null;
205
+ }
206
+ return {
207
+ 0: ch0,
208
+ 1: ch1,
209
+ 2: ch2,
210
+ 3: ch3,
211
+ totalBytes: cursor - start
212
+ };
213
+ }
214
+ function findByte(bytes, target, start) {
215
+ for (let i = start; i < bytes.length; i++) {
216
+ if (bytes[i] === target) return i;
217
+ }
218
+ return -1;
219
+ }
220
+ function asciiSubstring(bytes, start, end) {
221
+ let s = "";
222
+ for (let i = start; i < end; i++) {
223
+ s += String.fromCharCode(bytes[i]);
224
+ }
225
+ return s;
226
+ }
227
+ function hdrDecodeError(reason) {
228
+ return imageError({
229
+ code: "image-hdr-decode-failed",
230
+ reason
231
+ });
232
+ }
233
+ var LF;
234
+ var init_hdr_decoder = __esm({
235
+ "src/hdr-decoder.ts"() {
236
+ init_errors();
237
+ init_result();
238
+ LF = 10;
239
+ }
240
+ });
241
+ function resolveEncodeMode(mode, source) {
242
+ switch (mode) {
243
+ case "none":
244
+ return "none";
245
+ case "etc1s":
246
+ return "etc1s";
247
+ case "uastc":
248
+ return source.isHdr ? "uastc-hdr" : "uastc";
249
+ case "auto":
250
+ if (source.isHdr) return "uastc-hdr";
251
+ return source.colorSpace === "srgb" ? "etc1s" : "uastc";
252
+ }
253
+ }
254
+ function basisEncodeParamsFor(mode, source) {
255
+ const resolved = resolveEncodeMode(mode, source);
256
+ const srgbColor = source.colorSpace === "srgb";
257
+ switch (resolved) {
258
+ case "none":
259
+ return null;
260
+ case "etc1s":
261
+ return {
262
+ mode: "etc1s",
263
+ srgb: srgbColor,
264
+ perceptual: srgbColor,
265
+ uastcSupercompression: false,
266
+ mipGen: false
267
+ };
268
+ case "uastc":
269
+ return {
270
+ mode: "uastc-ldr",
271
+ srgb: srgbColor,
272
+ perceptual: srgbColor,
273
+ uastcSupercompression: true,
274
+ mipGen: false
275
+ };
276
+ case "uastc-hdr":
277
+ return {
278
+ mode: "uastc-hdr",
279
+ srgb: false,
280
+ perceptual: false,
281
+ uastcSupercompression: false,
282
+ mipGen: false
283
+ };
284
+ }
285
+ }
286
+ var MAX_ENCODE_SOURCE_PIXELS = 4096 * 4096;
287
+ async function encodeTextureToKtx2(pixels, width, height, mode, source) {
288
+ const params = basisEncodeParamsFor(mode, source);
289
+ if (params === null) {
290
+ return {
291
+ ok: false,
292
+ error: {
293
+ code: "ktx2-encode-failed",
294
+ mode: "none",
295
+ reason: "compressionMode resolved to none"
296
+ }
297
+ };
298
+ }
299
+ const resolved = resolveEncodeMode(mode, source);
300
+ if (width * height > MAX_ENCODE_SOURCE_PIXELS) {
301
+ return {
302
+ ok: false,
303
+ error: {
304
+ code: "ktx2-encode-source-too-large",
305
+ mode: resolved,
306
+ reason: `source ${width}x${height} (${width * height} px) exceeds the ${MAX_ENCODE_SOURCE_PIXELS} px (4096x4096) Basis encode ceiling; reduce the texture resolution to <=4096x4096 or set compressionMode:'none' in the sidecar to keep it uncompressed`
307
+ }
308
+ };
309
+ }
310
+ const result = await basisEncode(pixels, {
311
+ mode: params.mode,
312
+ width,
313
+ height,
314
+ srgb: params.srgb,
315
+ perceptual: params.perceptual,
316
+ uastcSupercompression: params.uastcSupercompression,
317
+ mipGen: params.mipGen
318
+ });
319
+ if (!result.ok) {
320
+ const reason = result.error.detail !== void 0 && "reason" in result.error.detail ? String(result.error.detail.reason) : result.error.code;
321
+ return {
322
+ ok: false,
323
+ error: {
324
+ code: result.error.code === "codec-init-failed" ? "codec-init-failed" : "ktx2-encode-failed",
325
+ mode: resolved,
326
+ reason
327
+ }
328
+ };
329
+ }
330
+ return { ok: true, value: { ktx2: result.value, mode: resolved } };
331
+ }
332
+
333
+ // src/parse-image.ts
334
+ init_errors();
335
+
336
+ // src/resize-image.ts
337
+ function downscaleRgba(bytes, width, height, maxDimension) {
338
+ if (width <= maxDimension && height <= maxDimension) {
339
+ return { bytes, width, height };
340
+ }
341
+ const scale = maxDimension / Math.max(width, height);
342
+ const targetWidth = Math.max(1, Math.round(width * scale));
343
+ const targetHeight = Math.max(1, Math.round(height * scale));
344
+ const output = new Uint8Array(targetWidth * targetHeight * 4);
345
+ const source = (x, y, channel) => bytes[(y * width + x) * 4 + channel] ?? 0;
346
+ for (let y = 0; y < targetHeight; y += 1) {
347
+ const sourceY = (y + 0.5) * height / targetHeight - 0.5;
348
+ const y0 = Math.max(0, Math.floor(sourceY));
349
+ const y1 = Math.min(height - 1, y0 + 1);
350
+ const yWeight = Math.max(0, Math.min(1, sourceY - y0));
351
+ for (let x = 0; x < targetWidth; x += 1) {
352
+ const sourceX = (x + 0.5) * width / targetWidth - 0.5;
353
+ const x0 = Math.max(0, Math.floor(sourceX));
354
+ const x1 = Math.min(width - 1, x0 + 1);
355
+ const xWeight = Math.max(0, Math.min(1, sourceX - x0));
356
+ const outputOffset = (y * targetWidth + x) * 4;
357
+ for (let channel = 0; channel < 4; channel += 1) {
358
+ const top = source(x0, y0, channel) * (1 - xWeight) + source(x1, y0, channel) * xWeight;
359
+ const bottom = source(x0, y1, channel) * (1 - xWeight) + source(x1, y1, channel) * xWeight;
360
+ output[outputOffset + channel] = Math.round(top * (1 - yWeight) + bottom * yWeight);
361
+ }
362
+ }
363
+ }
364
+ return { bytes: output, width: targetWidth, height: targetHeight };
365
+ }
366
+
367
+ // src/parse-image.ts
368
+ init_result();
369
+ var DEFAULT_MAX_DIMENSION = 16384;
370
+ var SUPPORTED_MIMES = ["image/png", "image/jpeg"];
371
+ function parseImage(bytes, mime, opts = {}) {
372
+ if (!SUPPORTED_MIMES.includes(mime)) {
373
+ return err(
374
+ imageError({
375
+ code: "image-format-unsupported",
376
+ actualMime: mime,
377
+ ...opts.path !== void 0 ? { path: opts.path } : {}
378
+ })
379
+ );
380
+ }
381
+ let width = 0;
382
+ let height = 0;
383
+ let rgba;
384
+ try {
385
+ if (mime === "image/png") {
386
+ const upngMod = UPNG.default ?? UPNG;
387
+ const decoded = upngMod.decode(bytes);
388
+ width = decoded.width;
389
+ height = decoded.height;
390
+ const frames = upngMod.toRGBA8(decoded);
391
+ const first = frames[0];
392
+ if (first === void 0) {
393
+ return err(
394
+ imageError({
395
+ code: "image-decode-failed",
396
+ reason: "UPNG.toRGBA8 returned no frames",
397
+ ...opts.path !== void 0 ? { path: opts.path } : {}
398
+ })
399
+ );
400
+ }
401
+ rgba = new Uint8Array(first);
402
+ } else {
403
+ const jpegMod = jpeg.default ?? jpeg;
404
+ const decoded = jpegMod.decode(bytes, { useTArray: true, formatAsRGBA: true });
405
+ width = decoded.width;
406
+ height = decoded.height;
407
+ rgba = decoded.data;
408
+ }
409
+ } catch (e) {
410
+ return err(
411
+ imageError({
412
+ code: "image-decode-failed",
413
+ reason: e instanceof Error ? e.message : String(e),
414
+ ...opts.path !== void 0 ? { path: opts.path } : {}
415
+ })
416
+ );
417
+ }
418
+ const downscaleLimit = opts.downscaleMaxDimension;
419
+ if (downscaleLimit !== void 0 && Number.isInteger(downscaleLimit) && downscaleLimit > 0) {
420
+ const downscaled = downscaleRgba(rgba, width, height, downscaleLimit);
421
+ rgba = downscaled.bytes;
422
+ width = downscaled.width;
423
+ height = downscaled.height;
424
+ }
425
+ const limit = opts.maxDimension ?? DEFAULT_MAX_DIMENSION;
426
+ if (width > limit || height > limit) {
427
+ return err(
428
+ imageError({
429
+ code: "image-dimension-out-of-bounds",
430
+ requested: { width, height },
431
+ limit
432
+ })
433
+ );
434
+ }
435
+ return ok({
436
+ bytes: rgba,
437
+ width,
438
+ height,
439
+ mime,
440
+ colorSpace: opts.colorSpace ?? "srgb",
441
+ mipmap: opts.mipmap ?? true
442
+ });
443
+ }
444
+
445
+ // src/image-importer.ts
446
+ function mimeFromSource(source) {
447
+ const lower = source.toLowerCase();
448
+ if (lower.endsWith(".png")) return "image/png";
449
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
450
+ return void 0;
451
+ }
452
+ function requiredImageOutputKind(source) {
453
+ const lower = source.toLowerCase();
454
+ if (lower.endsWith(".hdr")) return "equirect";
455
+ if (mimeFromSource(source) !== void 0 || lower.endsWith(".basis") || lower.endsWith(".ktx2")) {
456
+ return "texture";
457
+ }
458
+ return void 0;
459
+ }
460
+ function imageOutputTopologyActual(ctx) {
461
+ if (ctx.subAssets.length === 0) return "subAssets[] is empty";
462
+ return ctx.subAssets.map(
463
+ (sub, index) => `subAssets[${index}]=${sub.kind}:${sub.guid}:sourceIndex=${sub.sourceIndex}`
464
+ ).join(", ");
465
+ }
466
+ function imageConversionFailure(ctx, stage, ownerCode, sourcePath, expected, actual, hint) {
467
+ const diagnosticSourcePath = sourcePath.length === 0 ? ctx.source : sourcePath;
468
+ return new ImportError({
469
+ code: "source-validation-failed",
470
+ expected,
471
+ actual,
472
+ hint: IMPORT_ERROR_HINTS["source-validation-failed"],
473
+ detail: {
474
+ diagnostics: [
475
+ {
476
+ code: `image-conversion-${stage}-${ownerCode}`,
477
+ severity: "error",
478
+ sourcePath: diagnosticSourcePath,
479
+ sourceRange: { start: 0, end: 0, line: 1, column: 1 },
480
+ rule: `image-conversion-${stage}`,
481
+ expected,
482
+ actual,
483
+ hint
484
+ }
485
+ ]
486
+ }
487
+ });
488
+ }
489
+ function validateImageOutputTopology(ctx, requiredKind) {
490
+ if (ctx.subAssets.length === 1 && ctx.subAssets[0]?.kind === requiredKind && ctx.subAssets[0]?.sourceIndex === 0) {
491
+ return void 0;
492
+ }
493
+ const expected = `exactly one subAssets[] entry with kind "${requiredKind}" and sourceIndex 0`;
494
+ const actual = imageOutputTopologyActual(ctx);
495
+ return new ImportError({
496
+ code: "source-validation-failed",
497
+ expected,
498
+ actual,
499
+ hint: IMPORT_ERROR_HINTS["source-validation-failed"],
500
+ detail: {
501
+ diagnostics: [
502
+ {
503
+ code: "image-subasset-topology",
504
+ severity: "error",
505
+ sourcePath: `${ctx.source}#subAssets`,
506
+ sourceRange: { start: 0, end: 0, line: 1, column: 1 },
507
+ rule: "image-required-single-output",
508
+ expected,
509
+ actual,
510
+ hint: `declare exactly one ${requiredKind} sub-asset with sourceIndex 0 and remove foreign, duplicate, or misplaced entries`
511
+ }
512
+ ]
513
+ }
514
+ });
515
+ }
516
+ function mipmapTokenToBoolean(token) {
517
+ return token === "auto" || token === true;
518
+ }
519
+ function colorSpaceToFormat(colorSpace) {
520
+ return colorSpace === "srgb" ? "rgba8unorm-srgb" : "rgba8unorm";
521
+ }
522
+ async function inspectKtx2Source(ctx, bytes, metaColorSpace) {
523
+ const parsed = await parseKtx2(bytes);
524
+ if (!parsed.ok) {
525
+ return imageConversionFailure(
526
+ ctx,
527
+ "inspect",
528
+ "ktx2-parse",
529
+ ctx.source,
530
+ "a valid KTX2 container with a supported Basis profile",
531
+ `codec:${parsed.error.code}`,
532
+ "repair the KTX2 container and retry the image import"
533
+ );
534
+ }
535
+ const dfdColorSpace = ktx2ColorSpace(parsed.value);
536
+ if (dfdColorSpace === void 0) {
537
+ return imageConversionFailure(
538
+ ctx,
539
+ "inspect",
540
+ "ktx2-color-space-missing",
541
+ `${ctx.source}#DFD.transferFunction`,
542
+ "DFD transfer function to identify sRGB or linear color space",
543
+ "missing-or-unsupported-transfer-function",
544
+ "repair the KTX2 DFD color-space declaration before importing"
545
+ );
546
+ }
547
+ if (metaColorSpace !== void 0 && metaColorSpace !== dfdColorSpace) {
548
+ return imageConversionFailure(
549
+ ctx,
550
+ "inspect",
551
+ "ktx2-color-space-conflict",
552
+ `${ctx.source}#importSettings.colorSpace`,
553
+ "Meta.colorSpace to match the KTX2 DFD transfer function",
554
+ `meta=${metaColorSpace},dfd=${dfdColorSpace}`,
555
+ "repair Meta.colorSpace or re-encode the KTX2 source with matching color provenance"
556
+ );
557
+ }
558
+ const { pixelDepth, layerCount, faceCount } = parsed.value.header;
559
+ if (pixelDepth !== 0 || layerCount > 1 || faceCount !== 1) {
560
+ return imageConversionFailure(
561
+ ctx,
562
+ "inspect",
563
+ "ktx2-shape-unsupported",
564
+ `${ctx.source}#header`,
565
+ "a 2D, single-layer, single-face KTX2 texture",
566
+ `pixelDepth=${pixelDepth},layerCount=${layerCount},faceCount=${faceCount}`,
567
+ "re-encode the source as one 2D Basis texture"
568
+ );
569
+ }
570
+ const { initBasisTranscoder } = await import('@forgeax/engine-codec');
571
+ const module = await initBasisTranscoder();
572
+ let file;
573
+ try {
574
+ file = new module.KTX2File(bytes);
575
+ } catch {
576
+ return imageConversionFailure(
577
+ ctx,
578
+ "inspect",
579
+ "ktx2-invalid",
580
+ ctx.source,
581
+ "Basis KTX2File bytes to pass transcoder validation",
582
+ "basis-file-constructor-failed",
583
+ "repair or re-encode the KTX2 source before importing"
584
+ );
585
+ }
586
+ try {
587
+ try {
588
+ if (!file.isValid()) {
589
+ return imageConversionFailure(
590
+ ctx,
591
+ "inspect",
592
+ "ktx2-invalid",
593
+ ctx.source,
594
+ "Basis KTX2File bytes to pass transcoder validation",
595
+ "basis-file-invalid",
596
+ "repair or re-encode the KTX2 source before importing"
597
+ );
598
+ }
599
+ const profile = file.isETC1S() ? "etc1s" : file.isHDR() || file.isHDR4x4() ? "uastc-hdr" : file.isUASTC_LDR_4x4() ? "uastc-ldr" : void 0;
600
+ if (profile === void 0) {
601
+ return imageConversionFailure(
602
+ ctx,
603
+ "inspect",
604
+ "ktx2-profile-unsupported",
605
+ `${ctx.source}#Basis.profile`,
606
+ "ETC1S, UASTC-LDR, or UASTC-HDR Basis profile",
607
+ "unsupported-basis-profile",
608
+ "re-encode the source with ETC1S, UASTC-LDR, or UASTC-HDR"
609
+ );
610
+ }
611
+ if (profile === "uastc-hdr" && dfdColorSpace !== "linear") {
612
+ return imageConversionFailure(
613
+ ctx,
614
+ "inspect",
615
+ "ktx2-hdr-color-space-invalid",
616
+ `${ctx.source}#DFD.transferFunction`,
617
+ "UASTC-HDR source to use linear color space",
618
+ `profile=${profile},dfd=${dfdColorSpace}`,
619
+ "re-encode HDR data with a linear KTX2 DFD transfer function"
620
+ );
621
+ }
622
+ return {
623
+ colorSpace: dfdColorSpace,
624
+ profile,
625
+ width: file.getWidth(),
626
+ height: file.getHeight(),
627
+ levelCount: file.getLevels()
628
+ };
629
+ } catch {
630
+ return imageConversionFailure(
631
+ ctx,
632
+ "inspect",
633
+ "ktx2-invalid",
634
+ ctx.source,
635
+ "Basis KTX2File bytes to pass transcoder inspection",
636
+ "basis-inspection-threw",
637
+ "repair or re-encode the KTX2 source before importing"
638
+ );
639
+ }
640
+ } finally {
641
+ file.close();
642
+ }
643
+ }
644
+ async function importKtx2Source(ctx, bytes) {
645
+ const sourceKey = ctx.subAssets[0]?.sourceKey;
646
+ const metaColorSpace = sourceKey === void 0 ? ctx.importSettings.colorSpace : ctx.sourceOverrides?.[sourceKey]?.colorSpace;
647
+ const inspection = await inspectKtx2Source(
648
+ ctx,
649
+ bytes,
650
+ metaColorSpace === "srgb" || metaColorSpace === "linear" ? metaColorSpace : void 0
651
+ );
652
+ if (inspection instanceof ImportError) return { ok: false, error: inspection };
653
+ const out = [];
654
+ for (const sub of ctx.subAssets) {
655
+ if (sub.kind !== "texture" || sub.sourceIndex !== 0) continue;
656
+ out.push({
657
+ guid: sub.guid,
658
+ kind: "texture",
659
+ payload: {
660
+ kind: "texture",
661
+ width: inspection.width,
662
+ height: inspection.height,
663
+ format: colorSpaceToFormat(inspection.colorSpace),
664
+ data: bytes,
665
+ colorSpace: inspection.colorSpace,
666
+ mipmap: inspection.levelCount > 1,
667
+ mipLevelCount: inspection.levelCount
668
+ },
669
+ refs: [],
670
+ artifacts: {
671
+ body: {
672
+ mediaType: "image/ktx2",
673
+ assetCodec: {
674
+ name: "basis",
675
+ container: "ktx2",
676
+ profile: inspection.profile,
677
+ version: "1"
678
+ },
679
+ bytes
680
+ }
681
+ }
682
+ });
683
+ }
684
+ return { ok: true, value: { assets: out, sourceDependencies: [] } };
685
+ }
686
+ function basisMetaColorSpace(ctx) {
687
+ const sourceKey = ctx.subAssets[0]?.sourceKey;
688
+ const override = sourceKey === void 0 ? void 0 : ctx.sourceOverrides?.[sourceKey];
689
+ const value = override?.colorSpace ?? ctx.importSettings.colorSpace;
690
+ if (value !== "srgb" && value !== "linear") {
691
+ return imageConversionFailure(
692
+ ctx,
693
+ "inspect",
694
+ "basis-color-space-invalid",
695
+ `${ctx.source}#Meta.colorSpace`,
696
+ "Meta.colorSpace to be srgb or linear for raw Basis",
697
+ value === void 0 ? "missing" : `invalid:${String(value)}`,
698
+ "set Meta.colorSpace to srgb or linear before importing raw Basis bytes"
699
+ );
700
+ }
701
+ return value;
702
+ }
703
+ async function importBasisSource(ctx, bytes) {
704
+ const colorSpace = basisMetaColorSpace(ctx);
705
+ if (colorSpace instanceof ImportError) return { ok: false, error: colorSpace };
706
+ const { initBasisTranscoder, inspectBasisSource } = await import('@forgeax/engine-codec');
707
+ const module = await initBasisTranscoder();
708
+ if (module.BasisFile === void 0) {
709
+ throw new Error("basis-source-inspection-unavailable: transcoder lacks BasisFile");
710
+ }
711
+ let file;
712
+ try {
713
+ file = new module.BasisFile(bytes);
714
+ } catch {
715
+ return {
716
+ ok: false,
717
+ error: imageConversionFailure(
718
+ ctx,
719
+ "inspect",
720
+ "basis-source-invalid",
721
+ ctx.source,
722
+ "raw Basis bytes to pass transcoder validation",
723
+ "basis-file-invalid",
724
+ "repair or re-encode the raw Basis source before importing"
725
+ )
726
+ };
727
+ }
728
+ let inspection;
729
+ try {
730
+ try {
731
+ const format = file.getBasisTexFormat();
732
+ const profile = format === module.basis_tex_format.cETC1S.value ? "etc1s" : format === module.basis_tex_format.cUASTC_LDR_4x4.value ? "uastc-ldr" : void 0;
733
+ if (profile === void 0) {
734
+ return {
735
+ ok: false,
736
+ error: imageConversionFailure(
737
+ ctx,
738
+ "inspect",
739
+ "basis-profile-unsupported",
740
+ `${ctx.source}#Basis.profile`,
741
+ "ETC1S or UASTC-LDR raw Basis profile",
742
+ "unsupported-basis-profile",
743
+ "re-encode the source with ETC1S or UASTC-LDR"
744
+ )
745
+ };
746
+ }
747
+ const result = inspectBasisSource(file, { colorSpace }, profile);
748
+ if (!result.ok) {
749
+ return {
750
+ ok: false,
751
+ error: imageConversionFailure(
752
+ ctx,
753
+ "inspect",
754
+ "basis-inspection-failed",
755
+ ctx.source,
756
+ "raw Basis source metadata to pass codec inspection",
757
+ `codec:${result.error.code}`,
758
+ "repair the raw Basis source metadata and retry the import"
759
+ )
760
+ };
761
+ }
762
+ inspection = result.value;
763
+ } catch {
764
+ return {
765
+ ok: false,
766
+ error: imageConversionFailure(
767
+ ctx,
768
+ "inspect",
769
+ "basis-source-invalid",
770
+ ctx.source,
771
+ "raw Basis bytes to pass transcoder inspection",
772
+ "basis-inspection-threw",
773
+ "repair or re-encode the raw Basis source before importing"
774
+ )
775
+ };
776
+ }
777
+ } finally {
778
+ file.close();
779
+ }
780
+ if (inspection === void 0) {
781
+ return {
782
+ ok: false,
783
+ error: imageConversionFailure(
784
+ ctx,
785
+ "inspect",
786
+ "basis-inspection-failed",
787
+ ctx.source,
788
+ "raw Basis source metadata to be available after codec inspection",
789
+ "missing-inspection-result",
790
+ "repair the raw Basis source and retry the import"
791
+ )
792
+ };
793
+ }
794
+ const out = [];
795
+ for (const sub of ctx.subAssets) {
796
+ if (sub.kind !== "texture") continue;
797
+ if (sub.sourceIndex !== 0 || inspection.imageCount !== 1) {
798
+ return {
799
+ ok: false,
800
+ error: imageConversionFailure(
801
+ ctx,
802
+ "inspect",
803
+ "basis-image-shape-unsupported",
804
+ `${ctx.source}#Basis.images`,
805
+ "one raw Basis image at sourceIndex 0",
806
+ `imageCount=${inspection.imageCount},sourceIndex=${sub.sourceIndex}`,
807
+ "re-encode the source as one raw Basis image"
808
+ )
809
+ };
810
+ }
811
+ out.push({
812
+ guid: sub.guid,
813
+ kind: "texture",
814
+ payload: {
815
+ kind: "texture",
816
+ width: inspection.width,
817
+ height: inspection.height,
818
+ format: colorSpaceToFormat(colorSpace),
819
+ data: bytes,
820
+ colorSpace,
821
+ mipmap: inspection.levelCount > 1,
822
+ mipLevelCount: inspection.levelCount
823
+ },
824
+ refs: [],
825
+ artifacts: {
826
+ body: {
827
+ mediaType: "application/x-forgeax-basis",
828
+ assetCodec: {
829
+ name: "basis",
830
+ container: "basis",
831
+ profile: inspection.profile,
832
+ version: "1"
833
+ },
834
+ bytes
835
+ }
836
+ }
837
+ });
838
+ }
839
+ return { ok: true, value: { assets: out, sourceDependencies: [] } };
840
+ }
841
+ function compressionModeToken(token) {
842
+ if (token === "auto" || token === "etc1s" || token === "uastc" || token === "none") {
843
+ return token;
844
+ }
845
+ return "none";
846
+ }
847
+ async function maybeEncodeTextureBytes(ctx, pixels, width, height, compressionMode, colorSpace, isHdr) {
848
+ if (resolveEncodeMode(compressionMode, { colorSpace, isHdr }) === "none") {
849
+ return { ok: true, value: null };
850
+ }
851
+ const result = await encodeTextureToKtx2(pixels, width, height, compressionMode, {
852
+ colorSpace,
853
+ isHdr
854
+ });
855
+ if (!result.ok) {
856
+ return {
857
+ ok: false,
858
+ error: imageConversionFailure(
859
+ ctx,
860
+ "encode",
861
+ result.error.code === "ktx2-encode-source-too-large" ? "ktx2-source-too-large" : "ktx2-encode-refused",
862
+ `${ctx.source}#compressionMode`,
863
+ "the requested compression mode to accept the decoded image",
864
+ `codec:${result.error.code},mode:${result.error.mode}`,
865
+ result.error.code === "ktx2-encode-source-too-large" ? "reduce source dimensions or set compressionMode to none" : "repair the source image or compression settings and retry the import"
866
+ )
867
+ };
868
+ }
869
+ return { ok: true, value: result.value.ktx2 };
870
+ }
871
+ async function importImage(ctx) {
872
+ const requiredKind = requiredImageOutputKind(ctx.source);
873
+ if (requiredKind !== void 0) {
874
+ const topologyError = validateImageOutputTopology(ctx, requiredKind);
875
+ if (topologyError !== void 0) return { ok: false, error: topologyError };
876
+ }
877
+ const read = await ctx.readSource();
878
+ if (!read.ok) {
879
+ return {
880
+ ok: false,
881
+ error: new ImportError({
882
+ code: "source-read-failed",
883
+ expected: `readable source file at "${ctx.source}"`,
884
+ hint: IMPORT_ERROR_HINTS["source-read-failed"],
885
+ detail: {
886
+ source: ctx.source,
887
+ reason: read.error instanceof Error ? read.error.message : String(read.error)
888
+ }
889
+ })
890
+ };
891
+ }
892
+ const mime = mimeFromSource(ctx.source);
893
+ if (ctx.source.toLowerCase().endsWith(".basis")) {
894
+ return importBasisSource(ctx, read.value);
895
+ }
896
+ if (ctx.source.toLowerCase().endsWith(".ktx2")) {
897
+ return importKtx2Source(ctx, read.value);
898
+ }
899
+ if (mime === void 0 && ctx.source.toLowerCase().endsWith(".hdr")) {
900
+ const { decodeHdr: decodeHdr2 } = await Promise.resolve().then(() => (init_hdr_decoder(), hdr_decoder_exports));
901
+ const decoded2 = decodeHdr2(read.value);
902
+ if (!decoded2.ok) {
903
+ return {
904
+ ok: false,
905
+ error: imageConversionFailure(
906
+ ctx,
907
+ "decode",
908
+ "hdr",
909
+ ctx.source,
910
+ "valid Radiance RGBE bytes to decode into an equirect asset",
911
+ `image:${decoded2.error.code}`,
912
+ "repair the HDR header or pixel payload and retry the import"
913
+ )
914
+ };
915
+ }
916
+ const dec2 = decoded2.value;
917
+ const { halfFloat } = await import('@forgeax/engine-math');
918
+ const f16Bytes = halfFloat.f32ToF16Bytes(
919
+ new Uint8Array(dec2.data.buffer, dec2.data.byteOffset, dec2.data.byteLength)
920
+ );
921
+ const out2 = [];
922
+ for (const sub of ctx.subAssets) {
923
+ if (sub.kind !== "equirect") continue;
924
+ const payload = {
925
+ kind: "equirect",
926
+ width: dec2.width,
927
+ height: dec2.height,
928
+ format: "rgba16float",
929
+ data: f16Bytes,
930
+ colorSpace: "linear"
931
+ };
932
+ out2.push({
933
+ guid: sub.guid,
934
+ kind: "equirect",
935
+ payload,
936
+ refs: [],
937
+ artifacts: {
938
+ body: {
939
+ mediaType: "application/x-forgeax-rgba16f",
940
+ assetCodec: { name: "rgba16float" },
941
+ bytes: f16Bytes
942
+ }
943
+ }
944
+ });
945
+ }
946
+ return { ok: true, value: { assets: out2, sourceDependencies: [] } };
947
+ }
948
+ if (mime === void 0) {
949
+ throw new Error(
950
+ `imageImporter: unsupported source extension for "${ctx.source}" (expected .png / .jpg / .jpeg / .hdr / .ktx2 / .basis)`
951
+ );
952
+ }
953
+ const colorSpace = ctx.importSettings.colorSpace === "srgb" ? "srgb" : "linear";
954
+ const mipmap = mipmapTokenToBoolean(ctx.importSettings.mipmap);
955
+ const compressionMode = compressionModeToken(ctx.importSettings.compressionMode);
956
+ const downscaleMaxDimension = typeof ctx.importSettings.downscaleMaxDimension === "number" && Number.isInteger(ctx.importSettings.downscaleMaxDimension) && ctx.importSettings.downscaleMaxDimension > 0 ? ctx.importSettings.downscaleMaxDimension : void 0;
957
+ const decoded = parseImage(read.value, mime, {
958
+ colorSpace,
959
+ mipmap,
960
+ ...downscaleMaxDimension !== void 0 ? { downscaleMaxDimension } : {}
961
+ });
962
+ if (!decoded.ok) {
963
+ return {
964
+ ok: false,
965
+ error: imageConversionFailure(
966
+ ctx,
967
+ "decode",
968
+ "ldr",
969
+ ctx.source,
970
+ "valid PNG or JPEG bytes to decode into RGBA pixels",
971
+ `image:${decoded.error.code}`,
972
+ "repair the source image bytes and retry the import"
973
+ )
974
+ };
975
+ }
976
+ const dec = decoded.value;
977
+ const encoded = await maybeEncodeTextureBytes(
978
+ ctx,
979
+ dec.bytes,
980
+ dec.width,
981
+ dec.height,
982
+ compressionMode,
983
+ colorSpace,
984
+ false
985
+ );
986
+ if (!encoded.ok) return encoded;
987
+ const encodedBytes = encoded.value;
988
+ const out = [];
989
+ for (const sub of ctx.subAssets) {
990
+ if (sub.kind !== "texture") continue;
991
+ const payload = {
992
+ kind: "texture",
993
+ width: dec.width,
994
+ height: dec.height,
995
+ format: colorSpaceToFormat(colorSpace),
996
+ data: encodedBytes ?? dec.bytes,
997
+ colorSpace,
998
+ mipmap
999
+ };
1000
+ out.push({
1001
+ guid: sub.guid,
1002
+ kind: "texture",
1003
+ payload,
1004
+ refs: [],
1005
+ artifacts: {
1006
+ body: {
1007
+ mediaType: encodedBytes === null ? "application/x-forgeax-rgba8" : "image/ktx2",
1008
+ assetCodec: encodedBytes === null ? { name: "rgba8", version: "1" } : {
1009
+ name: "basis",
1010
+ container: "ktx2",
1011
+ profile: resolveEncodeMode(compressionMode, { colorSpace, isHdr: false })
1012
+ },
1013
+ bytes: encodedBytes ?? dec.bytes
1014
+ }
1015
+ }
1016
+ });
1017
+ }
1018
+ return { ok: true, value: { assets: out, sourceDependencies: [] } };
1019
+ }
1020
+ var decodeImageForImport = async (bytes, mimeType, importSettings) => {
1021
+ const colorSpace = importSettings.colorSpace === "srgb" || importSettings.colorSpace === "linear" ? importSettings.colorSpace : "linear";
1022
+ const mipmap = importSettings.mipmap === true;
1023
+ const downscaleMaxDimension = typeof importSettings.downscaleMaxDimension === "number" && Number.isInteger(importSettings.downscaleMaxDimension) && importSettings.downscaleMaxDimension > 0 ? importSettings.downscaleMaxDimension : void 0;
1024
+ const decoded = parseImage(bytes, mimeType, {
1025
+ colorSpace,
1026
+ mipmap,
1027
+ ...downscaleMaxDimension === void 0 ? {} : { downscaleMaxDimension }
1028
+ });
1029
+ if (!decoded.ok) return decoded;
1030
+ const tex = decoded.value;
1031
+ const requestedCompression = importSettings.compressionMode === "auto" || importSettings.compressionMode === "etc1s" || importSettings.compressionMode === "uastc" || importSettings.compressionMode === "none" ? importSettings.compressionMode : "none";
1032
+ const resolvedCompression = resolveEncodeMode(requestedCompression, {
1033
+ colorSpace,
1034
+ isHdr: false
1035
+ });
1036
+ let cookedBytes = tex.bytes;
1037
+ let mediaType = mimeType;
1038
+ let assetCodec = {
1039
+ name: "rgba8",
1040
+ version: "1"
1041
+ };
1042
+ if (resolvedCompression !== "none") {
1043
+ const encoded = await encodeTextureToKtx2(
1044
+ tex.bytes,
1045
+ tex.width,
1046
+ tex.height,
1047
+ requestedCompression,
1048
+ { colorSpace, isHdr: false }
1049
+ );
1050
+ if (!encoded.ok) {
1051
+ throw new Error(
1052
+ `embedded texture compression failed (${encoded.error.code} / ${encoded.error.mode}): ${encoded.error.reason}`
1053
+ );
1054
+ }
1055
+ cookedBytes = encoded.value.ktx2;
1056
+ mediaType = "image/ktx2";
1057
+ assetCodec = { name: "basis", profile: encoded.value.mode };
1058
+ }
1059
+ return {
1060
+ ok: true,
1061
+ value: {
1062
+ texture: {
1063
+ kind: "texture",
1064
+ data: cookedBytes,
1065
+ width: tex.width,
1066
+ height: tex.height,
1067
+ format: colorSpaceToFormat(colorSpace),
1068
+ colorSpace,
1069
+ mipmap
1070
+ },
1071
+ bytes: cookedBytes,
1072
+ mediaType,
1073
+ assetCodec
1074
+ }
1075
+ };
1076
+ };
1077
+ var imageImporter = {
1078
+ key: "image",
1079
+ import: importImage,
1080
+ capabilities: { decodeImage: decodeImageForImport }
1081
+ };
1082
+
1083
+ export { decodeImageForImport, imageImporter };
1084
+ //# sourceMappingURL=image-importer.mjs.map
1085
+ //# sourceMappingURL=image-importer.mjs.map