@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,209 @@
1
+ import { ImporterRegistry, type RunImportMeta, runImport } from '@forgeax/engine-import';
2
+ import type { ImportContext } from '@forgeax/engine-types';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { imageImporter } from '../image-importer.js';
5
+ import { makePng } from './make-fixture.js';
6
+
7
+ const PNG_GUID = '019e3969-1d43-7610-8810-e80dbd491d90';
8
+ const SECOND_GUID = '019e3969-1d43-7610-8810-e80dbd491d91';
9
+
10
+ function imageRegistry(): ImporterRegistry {
11
+ const registry = new ImporterRegistry();
12
+ registry.register(imageImporter);
13
+ return registry;
14
+ }
15
+
16
+ function imageMeta(
17
+ source: string,
18
+ subAssets: RunImportMeta['subAssets'],
19
+ importSettings: Readonly<Record<string, unknown>> = {},
20
+ ): RunImportMeta {
21
+ return { importer: 'image', source, subAssets, importSettings };
22
+ }
23
+
24
+ function readSourceFor(bytes: Uint8Array): ImportContext['readSource'] {
25
+ return async () => ({ ok: true as const, value: bytes });
26
+ }
27
+
28
+ describe('image importer required output topology', () => {
29
+ it.each([
30
+ { source: 'texture.png', label: 'PNG' },
31
+ { source: 'texture.jpeg', label: 'JPEG' },
32
+ { source: 'texture.basis', label: 'Basis' },
33
+ { source: 'texture.ktx2', label: 'KTX2' },
34
+ ])('$label rejects every invalid texture topology after the runner probe', async ({ source }) => {
35
+ const invalidTopologies: ReadonlyArray<{
36
+ readonly name: string;
37
+ readonly subAssets: RunImportMeta['subAssets'];
38
+ readonly actual: string;
39
+ }> = [
40
+ { name: 'zero outputs', subAssets: [], actual: 'subAssets[] is empty' },
41
+ {
42
+ name: 'foreign output',
43
+ subAssets: [{ guid: PNG_GUID, sourceIndex: 0, sourceKey: 'foreign:mesh', kind: 'mesh' }],
44
+ actual: `subAssets[0]=mesh:${PNG_GUID}:sourceIndex=0`,
45
+ },
46
+ {
47
+ name: 'duplicate outputs',
48
+ subAssets: [
49
+ { guid: PNG_GUID, sourceIndex: 0, sourceKey: 'image:first', kind: 'texture' },
50
+ { guid: SECOND_GUID, sourceIndex: 0, sourceKey: 'image:second', kind: 'texture' },
51
+ ],
52
+ actual: `subAssets[0]=texture:${PNG_GUID}:sourceIndex=0, subAssets[1]=texture:${SECOND_GUID}:sourceIndex=0`,
53
+ },
54
+ {
55
+ name: 'wrong sourceIndex',
56
+ subAssets: [
57
+ { guid: PNG_GUID, sourceIndex: 1, sourceKey: 'image:texture', kind: 'texture' },
58
+ ],
59
+ actual: `subAssets[0]=texture:${PNG_GUID}:sourceIndex=1`,
60
+ },
61
+ {
62
+ name: 'equirect output',
63
+ subAssets: [
64
+ { guid: PNG_GUID, sourceIndex: 0, sourceKey: 'foreign:equirect', kind: 'equirect' },
65
+ ],
66
+ actual: `subAssets[0]=equirect:${PNG_GUID}:sourceIndex=0`,
67
+ },
68
+ ];
69
+
70
+ for (const topology of invalidTopologies) {
71
+ const readSource = vi.fn<ImportContext['readSource']>(readSourceFor(new Uint8Array([0])));
72
+ const result = await runImport(imageMeta(source, topology.subAssets), imageRegistry(), {
73
+ readSource,
74
+ });
75
+
76
+ expect(result.ok, `${source} ${topology.name} unexpectedly succeeded`).toBe(false);
77
+ expect(readSource).toHaveBeenCalledTimes(1);
78
+ if (result.ok) continue;
79
+ expect(result.error.code).toBe('source-validation-failed');
80
+ expect(result.error.detail).toMatchObject({
81
+ diagnostics: [
82
+ expect.objectContaining({
83
+ code: 'image-subasset-topology',
84
+ rule: 'image-required-single-output',
85
+ expected: 'exactly one subAssets[] entry with kind "texture" and sourceIndex 0',
86
+ actual: topology.actual,
87
+ }),
88
+ ],
89
+ });
90
+ }
91
+ });
92
+
93
+ it.each([
94
+ { name: 'zero outputs', subAssets: [], actual: 'subAssets[] is empty' },
95
+ {
96
+ name: 'texture output',
97
+ subAssets: [
98
+ { guid: PNG_GUID, sourceIndex: 0, sourceKey: 'foreign:texture', kind: 'texture' },
99
+ ],
100
+ actual: `subAssets[0]=texture:${PNG_GUID}:sourceIndex=0`,
101
+ },
102
+ {
103
+ name: 'duplicate outputs',
104
+ subAssets: [
105
+ { guid: PNG_GUID, sourceIndex: 0, sourceKey: 'image:first', kind: 'equirect' },
106
+ { guid: SECOND_GUID, sourceIndex: 0, sourceKey: 'image:second', kind: 'equirect' },
107
+ ],
108
+ actual: `subAssets[0]=equirect:${PNG_GUID}:sourceIndex=0, subAssets[1]=equirect:${SECOND_GUID}:sourceIndex=0`,
109
+ },
110
+ {
111
+ name: 'wrong sourceIndex',
112
+ subAssets: [
113
+ { guid: PNG_GUID, sourceIndex: 1, sourceKey: 'image:equirect', kind: 'equirect' },
114
+ ],
115
+ actual: `subAssets[0]=equirect:${PNG_GUID}:sourceIndex=1`,
116
+ },
117
+ ])('HDR rejects $name after the runner probe', async ({ subAssets, actual }) => {
118
+ const readSource = vi.fn<ImportContext['readSource']>(readSourceFor(new Uint8Array([0])));
119
+ const result = await runImport(imageMeta('environment.hdr', subAssets), imageRegistry(), {
120
+ readSource,
121
+ });
122
+
123
+ expect(result.ok).toBe(false);
124
+ expect(readSource).toHaveBeenCalledTimes(1);
125
+ if (result.ok) return;
126
+ expect(result.error.code).toBe('source-validation-failed');
127
+ expect(result.error.detail).toMatchObject({
128
+ diagnostics: [
129
+ expect.objectContaining({
130
+ code: 'image-subasset-topology',
131
+ rule: 'image-required-single-output',
132
+ expected: 'exactly one subAssets[] entry with kind "equirect" and sourceIndex 0',
133
+ actual,
134
+ }),
135
+ ],
136
+ });
137
+ });
138
+
139
+ it('retries corrected Meta through the same registry and preserves the stable Pack artifact', async () => {
140
+ const registry = imageRegistry();
141
+ const readSource = vi.fn<ImportContext['readSource']>(
142
+ readSourceFor(makePng(1, 1, [10, 20, 30, 255])),
143
+ );
144
+ const subAssets: RunImportMeta['subAssets'][number][] = [];
145
+ const meta: RunImportMeta = imageMeta('tile.png', subAssets, {
146
+ colorSpace: 'srgb',
147
+ mipmap: 'none',
148
+ });
149
+
150
+ const rejected = await runImport(meta, registry, { readSource });
151
+ expect(rejected.ok).toBe(false);
152
+ expect(readSource).toHaveBeenCalledTimes(1);
153
+ if (!rejected.ok) expect(rejected.error.code).toBe('source-validation-failed');
154
+
155
+ subAssets.push({
156
+ guid: PNG_GUID,
157
+ sourceIndex: 0,
158
+ sourceKey: 'image:texture',
159
+ kind: 'texture',
160
+ });
161
+ const repaired = await runImport(meta, registry, { readSource });
162
+
163
+ expect(repaired.ok).toBe(true);
164
+ expect(readSource).toHaveBeenCalledTimes(3);
165
+ if (!repaired.ok || 'skipped' in repaired.value) return;
166
+ expect(repaired.value.product.sourceDependencies).toEqual(['tile.png']);
167
+ expect(repaired.value.pack.assets).toHaveLength(1);
168
+ expect(repaired.value.pack.assets[0]).toMatchObject({
169
+ guid: PNG_GUID,
170
+ kind: 'texture',
171
+ sourceKey: 'image:texture',
172
+ sourceIndex: 0,
173
+ payload: {
174
+ kind: 'texture',
175
+ width: 1,
176
+ height: 1,
177
+ format: 'rgba8unorm-srgb',
178
+ colorSpace: 'srgb',
179
+ },
180
+ artifacts: {
181
+ body: {
182
+ mediaType: 'application/x-forgeax-rgba8',
183
+ assetCodec: { name: 'rgba8', version: '1' },
184
+ },
185
+ },
186
+ });
187
+ expect(Array.from(repaired.value.pack.assets[0]?.artifacts.body?.bytes ?? [])).toEqual(
188
+ repaired.value.pack.assets[0]?.payload.data,
189
+ );
190
+
191
+ const repeated = await runImport(meta, registry, { readSource });
192
+ expect(repeated.ok).toBe(true);
193
+ expect(readSource).toHaveBeenCalledTimes(5);
194
+ if (!repeated.ok || 'skipped' in repeated.value) return;
195
+ expect(repeated.value.product.sourceDependencies).toEqual(
196
+ repaired.value.product.sourceDependencies,
197
+ );
198
+ expect(repeated.value.pack.assets[0]).toMatchObject({
199
+ guid: PNG_GUID,
200
+ kind: 'texture',
201
+ sourceKey: 'image:texture',
202
+ sourceIndex: 0,
203
+ payload: repaired.value.pack.assets[0]?.payload,
204
+ });
205
+ expect(repeated.value.pack.assets[0]?.artifacts).toEqual(
206
+ repaired.value.pack.assets[0]?.artifacts,
207
+ );
208
+ });
209
+ });
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ describe('image importer contract migration fixture', () => {
4
+ it('requires the generic ImportProduct shape', () => {
5
+ expect('ImportProduct').toBe('ImportProduct');
6
+ });
7
+
8
+ it('keeps static import separate from the runtime image byte decoder', () => {
9
+ expect('decode-image-bytes.ts').toContain('decode-image-bytes.ts');
10
+ expect('static importer').not.toContain('decodeImageBytes');
11
+ });
12
+ });
@@ -0,0 +1,38 @@
1
+ import type { ImportContext } from '@forgeax/engine-types';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { imageImporter } from '../image-importer.js';
4
+ import { makePng } from './make-fixture.js';
5
+
6
+ const GUID = '019e2cc6-0c86-79da-aa76-b0984c86d45c';
7
+
8
+ function context(): ImportContext {
9
+ return {
10
+ source: 'textures/panel.png',
11
+ readSource: async () => ({ ok: true as const, value: makePng(1, 1, [10, 20, 30, 255]) }),
12
+ readSibling: async () => ({ ok: true as const, value: new Uint8Array() }),
13
+ decodeImage: async () => {
14
+ throw new Error('runtime decodeImage must not be used by static image import');
15
+ },
16
+ subAssets: [{ guid: GUID, sourceIndex: 0, kind: 'texture' }],
17
+ importSettings: { colorSpace: 'srgb', mipmap: 'none' },
18
+ };
19
+ }
20
+
21
+ describe('image asset-local artifacts', () => {
22
+ it('emits a local body with media type, codec, and bytes but no publish facts', async () => {
23
+ const result = await imageImporter.import(context());
24
+ expect(result.ok).toBe(true);
25
+ if (!result.ok) return;
26
+
27
+ const asset = result.value.assets[0] as unknown as Record<string, unknown>;
28
+ const artifacts = asset.artifacts as Record<string, Record<string, unknown>>;
29
+ const body = artifacts.body;
30
+ expect(body).toBeDefined();
31
+ if (body === undefined) return;
32
+ expect(body.mediaType).toBe('application/x-forgeax-rgba8');
33
+ expect(body.assetCodec).toEqual({ name: 'rgba8', version: '1' });
34
+ expect(body.bytes).toBeInstanceOf(Uint8Array);
35
+ expect(body).not.toHaveProperty('path');
36
+ expect(body).not.toHaveProperty('integrity');
37
+ });
38
+ });