@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.
- package/LICENSE +202 -0
- package/README.md +166 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/compression-mode.unit.test.d.ts +2 -0
- package/dist/__tests__/compression-mode.unit.test.d.ts.map +1 -0
- package/dist/__tests__/encode-source-too-large.unit.test.d.ts +2 -0
- package/dist/__tests__/encode-source-too-large.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test-d.d.ts +2 -0
- package/dist/__tests__/errors.test-d.d.ts.map +1 -0
- package/dist/__tests__/image-importer-conversion-failure.unit.test.d.ts +2 -0
- package/dist/__tests__/image-importer-conversion-failure.unit.test.d.ts.map +1 -0
- package/dist/__tests__/image-importer-hdr-equirect.test.d.ts +2 -0
- package/dist/__tests__/image-importer-hdr-equirect.test.d.ts.map +1 -0
- package/dist/__tests__/image-importer-topology.unit.test.d.ts +2 -0
- package/dist/__tests__/image-importer-topology.unit.test.d.ts.map +1 -0
- package/dist/__tests__/image-importer.test.d.ts +2 -0
- package/dist/__tests__/image-importer.test.d.ts.map +1 -0
- package/dist/__tests__/image-local-artifacts.test.d.ts +2 -0
- package/dist/__tests__/image-local-artifacts.test.d.ts.map +1 -0
- package/dist/__tests__/image.unit.test.d.ts +2 -0
- package/dist/__tests__/image.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ktx2-basis-importer.unit.test.d.ts +2 -0
- package/dist/__tests__/ktx2-basis-importer.unit.test.d.ts.map +1 -0
- package/dist/__tests__/make-fixture.d.ts +12 -0
- package/dist/__tests__/make-fixture.d.ts.map +1 -0
- package/dist/__tests__/parse-image-downscale.test.d.ts +2 -0
- package/dist/__tests__/parse-image-downscale.test.d.ts.map +1 -0
- package/dist/__tests__/source-key.unit.test.d.ts +2 -0
- package/dist/__tests__/source-key.unit.test.d.ts.map +1 -0
- package/dist/decode-image-from-file.d.ts +40 -0
- package/dist/decode-image-from-file.d.ts.map +1 -0
- package/dist/decode-image-from-file.mjs +460 -0
- package/dist/decode-image-from-file.mjs.map +1 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/hdr-decoder.d.ts +28 -0
- package/dist/hdr-decoder.d.ts.map +1 -0
- package/dist/hdr-decoder.mjs +212 -0
- package/dist/hdr-decoder.mjs.map +1 -0
- package/dist/image-decoder-browser.d.ts +22 -0
- package/dist/image-decoder-browser.d.ts.map +1 -0
- package/dist/image-decoder-node.d.ts +50 -0
- package/dist/image-decoder-node.d.ts.map +1 -0
- package/dist/image-importer.d.ts +18 -0
- package/dist/image-importer.d.ts.map +1 -0
- package/dist/image-importer.mjs +1085 -0
- package/dist/image-importer.mjs.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +469 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ktx2-encode.d.ts +81 -0
- package/dist/ktx2-encode.d.ts.map +1 -0
- package/dist/ktx2-encode.mjs +98 -0
- package/dist/ktx2-encode.mjs.map +1 -0
- package/dist/parse-image.d.ts +47 -0
- package/dist/parse-image.d.ts.map +1 -0
- package/dist/parse-image.mjs +154 -0
- package/dist/parse-image.mjs.map +1 -0
- package/dist/reimport-reuse-meta.d.ts +81 -0
- package/dist/reimport-reuse-meta.d.ts.map +1 -0
- package/dist/resize-image.d.ts +7 -0
- package/dist/resize-image.d.ts.map +1 -0
- package/dist/result.d.ts +2 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/runtime/asset-decoders.d.ts +4 -0
- package/dist/runtime/asset-decoders.d.ts.map +1 -0
- package/dist/source-key.d.ts +21 -0
- package/dist/source-key.d.ts.map +1 -0
- package/dist/sub-asset-key.d.ts +50 -0
- package/dist/sub-asset-key.d.ts.map +1 -0
- package/dist/to-asset-pack.d.ts +61 -0
- package/dist/to-asset-pack.d.ts.map +1 -0
- package/package.json +105 -0
- package/src/__tests__/compression-mode.unit.test.ts +104 -0
- package/src/__tests__/encode-source-too-large.unit.test.ts +81 -0
- package/src/__tests__/errors.test-d.ts +148 -0
- package/src/__tests__/image-importer-conversion-failure.unit.test.ts +297 -0
- package/src/__tests__/image-importer-hdr-equirect.test.ts +218 -0
- package/src/__tests__/image-importer-topology.unit.test.ts +209 -0
- package/src/__tests__/image-importer.test.ts +12 -0
- package/src/__tests__/image-local-artifacts.test.ts +38 -0
- package/src/__tests__/image.unit.test.ts +1253 -0
- package/src/__tests__/ktx2-basis-importer.unit.test.ts +126 -0
- package/src/__tests__/make-fixture.ts +100 -0
- package/src/__tests__/parse-image-downscale.test.ts +26 -0
- package/src/__tests__/source-key.unit.test.ts +65 -0
- package/src/decode-image-from-file.ts +233 -0
- package/src/errors.ts +60 -0
- package/src/hdr-decoder.ts +255 -0
- package/src/image-decoder-browser.ts +88 -0
- package/src/image-decoder-node.ts +67 -0
- package/src/image-decoders.d.ts +23 -0
- package/src/image-importer.ts +858 -0
- package/src/index.ts +44 -0
- package/src/ktx2-encode.ts +217 -0
- package/src/parse-image.ts +176 -0
- package/src/reimport-reuse-meta.ts +160 -0
- package/src/resize-image.ts +39 -0
- package/src/result.ts +13 -0
- package/src/runtime/asset-decoders.ts +152 -0
- package/src/source-key.ts +41 -0
- package/src/sub-asset-key.ts +67 -0
- package/src/to-asset-pack.ts +95 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ktx2-encode.d.ts","sourceRoot":"","sources":["../src/ktx2-encode.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAGpE,uEAAuE;AACvE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAElE,oFAAoF;AACpF,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,OAAO,GAAG,WAAW,GAAG,MAAM,CAAC;AAE1E,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,CAAC;IACvC,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,gBAAgB,GACvB,kBAAkB,CAYpB;AAED,2DAA2D;AAC3D,MAAM,WAAW,iBAAiB;IAChC,wCAAwC;IACxC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,sCAAsC;IACtC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,gEAAgE;IAChE,QAAQ,CAAC,qBAAqB,EAAE,OAAO,CAAC;IACxC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,gBAAgB,GACvB,iBAAiB,GAAG,IAAI,CA+B1B;AAED,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;CACnC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,QAAc,CAAC;AAEpD,gFAAgF;AAChF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,oBAAoB,GAAG,mBAAmB,GAAG,8BAA8B,CAAC;IAC3F,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAA;CAAE,CAAC;AAE1D;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,mBAAmB,CAAC,CAuD9B"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { basisEncode } from '@forgeax/engine-codec/encode';
|
|
2
|
+
|
|
3
|
+
// src/ktx2-encode.ts
|
|
4
|
+
function resolveEncodeMode(mode, source) {
|
|
5
|
+
switch (mode) {
|
|
6
|
+
case "none":
|
|
7
|
+
return "none";
|
|
8
|
+
case "etc1s":
|
|
9
|
+
return "etc1s";
|
|
10
|
+
case "uastc":
|
|
11
|
+
return source.isHdr ? "uastc-hdr" : "uastc";
|
|
12
|
+
case "auto":
|
|
13
|
+
if (source.isHdr) return "uastc-hdr";
|
|
14
|
+
return source.colorSpace === "srgb" ? "etc1s" : "uastc";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function basisEncodeParamsFor(mode, source) {
|
|
18
|
+
const resolved = resolveEncodeMode(mode, source);
|
|
19
|
+
const srgbColor = source.colorSpace === "srgb";
|
|
20
|
+
switch (resolved) {
|
|
21
|
+
case "none":
|
|
22
|
+
return null;
|
|
23
|
+
case "etc1s":
|
|
24
|
+
return {
|
|
25
|
+
mode: "etc1s",
|
|
26
|
+
srgb: srgbColor,
|
|
27
|
+
perceptual: srgbColor,
|
|
28
|
+
uastcSupercompression: false,
|
|
29
|
+
mipGen: false
|
|
30
|
+
};
|
|
31
|
+
case "uastc":
|
|
32
|
+
return {
|
|
33
|
+
mode: "uastc-ldr",
|
|
34
|
+
srgb: srgbColor,
|
|
35
|
+
perceptual: srgbColor,
|
|
36
|
+
uastcSupercompression: true,
|
|
37
|
+
mipGen: false
|
|
38
|
+
};
|
|
39
|
+
case "uastc-hdr":
|
|
40
|
+
return {
|
|
41
|
+
mode: "uastc-hdr",
|
|
42
|
+
srgb: false,
|
|
43
|
+
perceptual: false,
|
|
44
|
+
uastcSupercompression: false,
|
|
45
|
+
mipGen: false
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
var MAX_ENCODE_SOURCE_PIXELS = 4096 * 4096;
|
|
50
|
+
async function encodeTextureToKtx2(pixels, width, height, mode, source) {
|
|
51
|
+
const params = basisEncodeParamsFor(mode, source);
|
|
52
|
+
if (params === null) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
error: {
|
|
56
|
+
code: "ktx2-encode-failed",
|
|
57
|
+
mode: "none",
|
|
58
|
+
reason: "compressionMode resolved to none"
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const resolved = resolveEncodeMode(mode, source);
|
|
63
|
+
if (width * height > MAX_ENCODE_SOURCE_PIXELS) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
error: {
|
|
67
|
+
code: "ktx2-encode-source-too-large",
|
|
68
|
+
mode: resolved,
|
|
69
|
+
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`
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const result = await basisEncode(pixels, {
|
|
74
|
+
mode: params.mode,
|
|
75
|
+
width,
|
|
76
|
+
height,
|
|
77
|
+
srgb: params.srgb,
|
|
78
|
+
perceptual: params.perceptual,
|
|
79
|
+
uastcSupercompression: params.uastcSupercompression,
|
|
80
|
+
mipGen: params.mipGen
|
|
81
|
+
});
|
|
82
|
+
if (!result.ok) {
|
|
83
|
+
const reason = result.error.detail !== void 0 && "reason" in result.error.detail ? String(result.error.detail.reason) : result.error.code;
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
error: {
|
|
87
|
+
code: result.error.code === "codec-init-failed" ? "codec-init-failed" : "ktx2-encode-failed",
|
|
88
|
+
mode: resolved,
|
|
89
|
+
reason
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, value: { ktx2: result.value, mode: resolved } };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { MAX_ENCODE_SOURCE_PIXELS, basisEncodeParamsFor, encodeTextureToKtx2, resolveEncodeMode };
|
|
97
|
+
//# sourceMappingURL=ktx2-encode.mjs.map
|
|
98
|
+
//# sourceMappingURL=ktx2-encode.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ktx2-encode.ts"],"names":[],"mappings":"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","file":"ktx2-encode.mjs","sourcesContent":["// ktx2-encode.ts -- the image-side offline texture encode arm (M3 w18, D-5).\n//\n// This module is the image-semantic layer of the Basis encode path: it maps the\n// sidecar `compressionMode` control-plane value + the source's colorSpace / HDR\n// signal into a delivery encoding (D-12 'auto' derivation), maps that to the\n// encoder parameters, and drives the low-level `basisEncode` binding that lives\n// in `@forgeax/engine-codec/encode` (the WASM + enum plumbing stays in codec;\n// the image semantics stay here -- D-5 \"arm in image, binary in codec/encode\").\n//\n// It is a NODE-ONLY build-time module (consumed by imageImporter, itself a\n// node-only sub-export). It never touches GPU (isolation path b) and is never\n// bundled into the runtime (isolation path a/d: runtime does not import\n// @forgeax/engine-image nor @forgeax/engine-codec/encode).\n//\n// M3 sequencing constraint (plan R-9): the 'auto' derivation is implemented and\n// tested here, but the sidecar DEFAULT stays 'none'. The default flip to 'auto'\n// is M5 (w38); doing it here would make every existing texture encode to Basis\n// ahead of loader support and redden the whole fixture fleet.\n\nimport type { BasisEncodeMode } from '@forgeax/engine-codec/encode';\nimport { basisEncode } from '@forgeax/engine-codec/encode';\n\n/** The sidecar control-plane value (ImageMetadata.compressionMode). */\nexport type CompressionMode = 'auto' | 'etc1s' | 'uastc' | 'none';\n\n/** The resolved delivery encoding after 'auto' derivation. `'none'` = no encode. */\nexport type ResolvedEncodeMode = 'etc1s' | 'uastc' | 'uastc-hdr' | 'none';\n\n/** Source signals that drive 'auto' derivation (D-12) and encoder params. */\nexport interface EncodeSourceInfo {\n /** Color transfer of the source (from the sidecar importSettings). */\n readonly colorSpace: 'srgb' | 'linear';\n /** Whether the source is an HDR image (.hdr -> rgba16float). */\n readonly isHdr: boolean;\n}\n\n/**\n * Resolve the sidecar `compressionMode` into a concrete delivery encoding.\n *\n * D-12 'auto' derivation (zero new required inputs, derived from existing\n * sidecar signals):\n * - HDR source -> 'uastc-hdr' (BC6H target downstream)\n * - colorSpace 'srgb' -> 'etc1s' (albedo / UI color)\n * - colorSpace 'linear' -> 'uastc' (normals / ORM / data)\n *\n * Explicit modes pass through, except an explicit 'uastc' on an HDR source\n * resolves to 'uastc-hdr' (there is no LDR UASTC path for HDR pixels). 'none'\n * and 'etc1s' pass through verbatim.\n */\nexport function resolveEncodeMode(\n mode: CompressionMode,\n source: EncodeSourceInfo,\n): ResolvedEncodeMode {\n switch (mode) {\n case 'none':\n return 'none';\n case 'etc1s':\n return 'etc1s';\n case 'uastc':\n return source.isHdr ? 'uastc-hdr' : 'uastc';\n case 'auto':\n if (source.isHdr) return 'uastc-hdr';\n return source.colorSpace === 'srgb' ? 'etc1s' : 'uastc';\n }\n}\n\n/** Encoder parameters for a resolved delivery encoding. */\nexport interface BasisEncodeParams {\n /** The codec-side delivery encoding. */\n readonly mode: BasisEncodeMode;\n /** Write the sRGB transfer function (LDR color only). */\n readonly srgb: boolean;\n /** ETC1S perceptual (sRGB) metric. */\n readonly perceptual: boolean;\n /** Wrap the UASTC-LDR payload in KTX2 zstd supercompression. */\n readonly uastcSupercompression: boolean;\n /** Encoder-side mip generation. M3 keeps this false (offline mips land in M5). */\n readonly mipGen: boolean;\n}\n\n/**\n * Map the sidecar mode + source into encoder parameters. Returns `null` for the\n * 'none' path -- the caller keeps the uncompressed `.bin` path and never calls\n * the encoder.\n */\nexport function basisEncodeParamsFor(\n mode: CompressionMode,\n source: EncodeSourceInfo,\n): BasisEncodeParams | null {\n const resolved = resolveEncodeMode(mode, source);\n const srgbColor = source.colorSpace === 'srgb';\n switch (resolved) {\n case 'none':\n return null;\n case 'etc1s':\n return {\n mode: 'etc1s',\n srgb: srgbColor,\n perceptual: srgbColor,\n uastcSupercompression: false,\n mipGen: false,\n };\n case 'uastc':\n return {\n mode: 'uastc-ldr',\n srgb: srgbColor,\n perceptual: srgbColor,\n uastcSupercompression: true,\n mipGen: false,\n };\n case 'uastc-hdr':\n return {\n mode: 'uastc-hdr',\n srgb: false,\n perceptual: false,\n uastcSupercompression: false,\n mipGen: false,\n };\n }\n}\n\n/** A successful encode result: KTX2 bytes + the resolved delivery encoding. */\nexport interface EncodedTexture {\n readonly ktx2: Uint8Array;\n readonly mode: ResolvedEncodeMode;\n}\n\n/**\n * Maximum source pixels the wasm32 Basis encoder accepts (4096x4096 = 16.78 Mpx\n * = 1024*1024*16). This mirrors the raised `BASISU_ENCODER_MAX_SOURCE_IMAGE_PIXELS`\n * ceiling patched into the encoder WASM (packages/codec/scripts/build-wasm.mjs).\n * Sources above this are rejected fast with a structured `ktx2-encode-source-too-large`\n * error rather than reaching the encoder and getting a silent 0-byte failure.\n */\nexport const MAX_ENCODE_SOURCE_PIXELS = 4096 * 4096;\n\n/** Failure result carrying a short reason (encoder-side or invalid request). */\nexport interface EncodeFailure {\n readonly code: 'ktx2-encode-failed' | 'codec-init-failed' | 'ktx2-encode-source-too-large';\n readonly mode: ResolvedEncodeMode;\n readonly reason: string;\n}\n\nexport type EncodeTextureResult =\n | { readonly ok: true; readonly value: EncodedTexture }\n | { readonly ok: false; readonly error: EncodeFailure };\n\n/**\n * Encode source pixels into a Basis KTX2 texture per the sidecar mode.\n *\n * `pixels` is tight-packed RGBA: 8-bit RGBA for LDR sources, rgba16float bytes\n * for HDR sources. Returns `{ ok: false }` with `mode: 'none'` if the sidecar\n * mode resolves to 'none' -- the importer treats that as \"no encode, keep the\n * .bin path\" rather than an error (see importer callsite w18).\n */\nexport async function encodeTextureToKtx2(\n pixels: Uint8Array,\n width: number,\n height: number,\n mode: CompressionMode,\n source: EncodeSourceInfo,\n): Promise<EncodeTextureResult> {\n const params = basisEncodeParamsFor(mode, source);\n if (params === null) {\n return {\n ok: false,\n error: {\n code: 'ktx2-encode-failed',\n mode: 'none',\n reason: 'compressionMode resolved to none',\n },\n };\n }\n const resolved = resolveEncodeMode(mode, source);\n // Fail fast above the wasm32 encoder's source-pixel ceiling (4096^2). Above\n // this the encoder returns 0 bytes silently; surface a structured, actionable\n // error instead so the importer reports \"reduce resolution or set\n // compressionMode:'none'\" rather than an opaque encode failure.\n if (width * height > MAX_ENCODE_SOURCE_PIXELS) {\n return {\n ok: false,\n error: {\n code: 'ktx2-encode-source-too-large',\n mode: resolved,\n reason:\n `source ${width}x${height} (${width * height} px) exceeds the ${MAX_ENCODE_SOURCE_PIXELS} px ` +\n '(4096x4096) Basis encode ceiling; reduce the texture resolution to <=4096x4096 ' +\n \"or set compressionMode:'none' in the sidecar to keep it uncompressed\",\n },\n };\n }\n const result = await basisEncode(pixels, {\n mode: params.mode,\n width,\n height,\n srgb: params.srgb,\n perceptual: params.perceptual,\n uastcSupercompression: params.uastcSupercompression,\n mipGen: params.mipGen,\n });\n if (!result.ok) {\n const reason =\n result.error.detail !== undefined && 'reason' in result.error.detail\n ? String((result.error.detail as { reason: unknown }).reason)\n : result.error.code;\n return {\n ok: false,\n error: {\n code:\n result.error.code === 'codec-init-failed' ? 'codec-init-failed' : 'ktx2-encode-failed',\n mode: resolved,\n reason,\n },\n };\n }\n return { ok: true, value: { ktx2: result.value, mode: resolved } };\n}\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DecodedImage, ImageColorSpace, ImageError } from '@forgeax/engine-types';
|
|
2
|
+
import type { Result } from './result.js';
|
|
3
|
+
export interface ParseImageOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Maximum allowed dimension (width or height). Defaults to 16384 -- the
|
|
6
|
+
* conservative WebGPU `maxTextureDimension2D` floor across desktop +
|
|
7
|
+
* high-end mobile (research F-3 / spec). Test fixtures pass smaller
|
|
8
|
+
* values to exercise the bounds-check code path with tiny fixtures.
|
|
9
|
+
*/
|
|
10
|
+
readonly maxDimension?: number;
|
|
11
|
+
/** Optional asset-owned cooked-payload target; source bytes remain unchanged. */
|
|
12
|
+
readonly downscaleMaxDimension?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Sidecar `colorSpace` carried over to the DecodedImage POD. Defaults to
|
|
15
|
+
* 'srgb' (typical baseColor / albedo path; plan-strategy section 2.5).
|
|
16
|
+
*/
|
|
17
|
+
readonly colorSpace?: ImageColorSpace;
|
|
18
|
+
/**
|
|
19
|
+
* Sidecar `mipmap` flag carried over to DecodedImage. Defaults to true
|
|
20
|
+
* (auto-generate via runtime mipmap-generator; plan-strategy section 2.6).
|
|
21
|
+
*/
|
|
22
|
+
readonly mipmap?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Optional source path to embed in error.detail.path (decode-failed /
|
|
25
|
+
* format-unsupported variants). decodeImageFromFile fills this in;
|
|
26
|
+
* in-memory parseImage callers leave it empty.
|
|
27
|
+
*/
|
|
28
|
+
readonly path?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Pure synchronous image decoder. Translates raw byte stream + mime hint
|
|
32
|
+
* into a tight-packed RGBA `DecodedImage` POD. Surfaces all four
|
|
33
|
+
* ImageErrorCode members through the structured `Result<DecodedImage,
|
|
34
|
+
* ImageError>` return (charter P3 explicit failure; AGENTS.md "Return
|
|
35
|
+
* Result, never throw").
|
|
36
|
+
*
|
|
37
|
+
* Decoder selection follows the mime hint:
|
|
38
|
+
* - `image/png` -> upng-js `UPNG.decode` + `UPNG.toRGBA8`
|
|
39
|
+
* - `image/jpeg` -> jpeg-js `decode` with `formatAsRGBA: true`
|
|
40
|
+
* - other -> `image-format-unsupported`
|
|
41
|
+
*
|
|
42
|
+
* Decoded bytes are always tight-packed RGBA (`bytes.length === width *
|
|
43
|
+
* height * 4`). `colorSpace` and `mipmap` are carried from the supplied
|
|
44
|
+
* `ParseImageOptions` (decodeImageFromFile passes the sidecar settings).
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseImage(bytes: Uint8Array, mime: string, opts?: ParseImageOptions): Result<DecodedImage, ImageError>;
|
|
47
|
+
//# sourceMappingURL=parse-image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-image.d.ts","sourceRoot":"","sources":["../src/parse-image.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAUvF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAE/B,iFAAiF;IACjF,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAExC;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,eAAe,CAAC;IAEtC;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAE1B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAkBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,iBAAsB,GAC3B,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CA8FlC"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as jpeg from 'jpeg-js';
|
|
2
|
+
import * as UPNG from 'upng-js';
|
|
3
|
+
import { err, ok, IMAGE_ERROR_HINTS } from '@forgeax/engine-types';
|
|
4
|
+
|
|
5
|
+
// src/parse-image.ts
|
|
6
|
+
var IMAGE_ERROR_EXPECTED = {
|
|
7
|
+
"image-decode-failed": "PNG / JPG byte stream decodes successfully",
|
|
8
|
+
"image-format-unsupported": "mime is one of ['image/png', 'image/jpeg']; uploadTexture format <-> colorSpace family agrees",
|
|
9
|
+
"image-dimension-out-of-bounds": "width and height fall under device caps maxTextureDimension2D (or 16384 hard cap)",
|
|
10
|
+
"image-meta-missing": "<source>.meta.json sidecar (importer: 'image') exists in the same directory",
|
|
11
|
+
"image-hdr-decode-failed": "Radiance RGBE header is valid and pixel data decodes successfully",
|
|
12
|
+
// feat-20260521-sprite-atlas-animation M1 T-03 — vite-plugin-image atlas
|
|
13
|
+
// hook .expected literals (plan-strategy section 2 D-2 + AC-10 a/b/c).
|
|
14
|
+
// ImageErrorImpl construction path is unchanged: the new atlas-* errors
|
|
15
|
+
// flow through `new ImageErrorImpl({ code: 'atlas-...', ...detail })` and
|
|
16
|
+
// pick the .expected string up from this Record at construction time
|
|
17
|
+
// (charter P3 explicit failure SSOT — AI users surface .expected next to
|
|
18
|
+
// .hint after switch (err.code) without parsing the message).
|
|
19
|
+
"atlas-empty-input": "images.length >= 1",
|
|
20
|
+
"atlas-size-exceeded": "image width x height <= maxAtlasSize^2 and each image fits in the atlas footprint",
|
|
21
|
+
"atlas-region-mismatch": "sum(regions[i].w x regions[i].h) <= atlasWidth x atlasHeight"
|
|
22
|
+
};
|
|
23
|
+
var ImageErrorImpl = class extends Error {
|
|
24
|
+
code;
|
|
25
|
+
expected;
|
|
26
|
+
hint;
|
|
27
|
+
detail;
|
|
28
|
+
constructor(detail) {
|
|
29
|
+
const code = detail.code;
|
|
30
|
+
const expected = IMAGE_ERROR_EXPECTED[code];
|
|
31
|
+
const hint = IMAGE_ERROR_HINTS[code];
|
|
32
|
+
super(`[ImageError ${code}] expected: ${expected}; hint: ${hint}`);
|
|
33
|
+
this.name = "ImageError";
|
|
34
|
+
this.code = code;
|
|
35
|
+
this.expected = expected;
|
|
36
|
+
this.hint = hint;
|
|
37
|
+
this.detail = detail;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function imageError(detail) {
|
|
41
|
+
return new ImageErrorImpl(detail);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/resize-image.ts
|
|
45
|
+
function downscaleRgba(bytes, width, height, maxDimension) {
|
|
46
|
+
if (width <= maxDimension && height <= maxDimension) {
|
|
47
|
+
return { bytes, width, height };
|
|
48
|
+
}
|
|
49
|
+
const scale = maxDimension / Math.max(width, height);
|
|
50
|
+
const targetWidth = Math.max(1, Math.round(width * scale));
|
|
51
|
+
const targetHeight = Math.max(1, Math.round(height * scale));
|
|
52
|
+
const output = new Uint8Array(targetWidth * targetHeight * 4);
|
|
53
|
+
const source = (x, y, channel) => bytes[(y * width + x) * 4 + channel] ?? 0;
|
|
54
|
+
for (let y = 0; y < targetHeight; y += 1) {
|
|
55
|
+
const sourceY = (y + 0.5) * height / targetHeight - 0.5;
|
|
56
|
+
const y0 = Math.max(0, Math.floor(sourceY));
|
|
57
|
+
const y1 = Math.min(height - 1, y0 + 1);
|
|
58
|
+
const yWeight = Math.max(0, Math.min(1, sourceY - y0));
|
|
59
|
+
for (let x = 0; x < targetWidth; x += 1) {
|
|
60
|
+
const sourceX = (x + 0.5) * width / targetWidth - 0.5;
|
|
61
|
+
const x0 = Math.max(0, Math.floor(sourceX));
|
|
62
|
+
const x1 = Math.min(width - 1, x0 + 1);
|
|
63
|
+
const xWeight = Math.max(0, Math.min(1, sourceX - x0));
|
|
64
|
+
const outputOffset = (y * targetWidth + x) * 4;
|
|
65
|
+
for (let channel = 0; channel < 4; channel += 1) {
|
|
66
|
+
const top = source(x0, y0, channel) * (1 - xWeight) + source(x1, y0, channel) * xWeight;
|
|
67
|
+
const bottom = source(x0, y1, channel) * (1 - xWeight) + source(x1, y1, channel) * xWeight;
|
|
68
|
+
output[outputOffset + channel] = Math.round(top * (1 - yWeight) + bottom * yWeight);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { bytes: output, width: targetWidth, height: targetHeight };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/parse-image.ts
|
|
76
|
+
var DEFAULT_MAX_DIMENSION = 16384;
|
|
77
|
+
var SUPPORTED_MIMES = ["image/png", "image/jpeg"];
|
|
78
|
+
function parseImage(bytes, mime, opts = {}) {
|
|
79
|
+
if (!SUPPORTED_MIMES.includes(mime)) {
|
|
80
|
+
return err(
|
|
81
|
+
imageError({
|
|
82
|
+
code: "image-format-unsupported",
|
|
83
|
+
actualMime: mime,
|
|
84
|
+
...opts.path !== void 0 ? { path: opts.path } : {}
|
|
85
|
+
})
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
let width = 0;
|
|
89
|
+
let height = 0;
|
|
90
|
+
let rgba;
|
|
91
|
+
try {
|
|
92
|
+
if (mime === "image/png") {
|
|
93
|
+
const upngMod = UPNG.default ?? UPNG;
|
|
94
|
+
const decoded = upngMod.decode(bytes);
|
|
95
|
+
width = decoded.width;
|
|
96
|
+
height = decoded.height;
|
|
97
|
+
const frames = upngMod.toRGBA8(decoded);
|
|
98
|
+
const first = frames[0];
|
|
99
|
+
if (first === void 0) {
|
|
100
|
+
return err(
|
|
101
|
+
imageError({
|
|
102
|
+
code: "image-decode-failed",
|
|
103
|
+
reason: "UPNG.toRGBA8 returned no frames",
|
|
104
|
+
...opts.path !== void 0 ? { path: opts.path } : {}
|
|
105
|
+
})
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
rgba = new Uint8Array(first);
|
|
109
|
+
} else {
|
|
110
|
+
const jpegMod = jpeg.default ?? jpeg;
|
|
111
|
+
const decoded = jpegMod.decode(bytes, { useTArray: true, formatAsRGBA: true });
|
|
112
|
+
width = decoded.width;
|
|
113
|
+
height = decoded.height;
|
|
114
|
+
rgba = decoded.data;
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
return err(
|
|
118
|
+
imageError({
|
|
119
|
+
code: "image-decode-failed",
|
|
120
|
+
reason: e instanceof Error ? e.message : String(e),
|
|
121
|
+
...opts.path !== void 0 ? { path: opts.path } : {}
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const downscaleLimit = opts.downscaleMaxDimension;
|
|
126
|
+
if (downscaleLimit !== void 0 && Number.isInteger(downscaleLimit) && downscaleLimit > 0) {
|
|
127
|
+
const downscaled = downscaleRgba(rgba, width, height, downscaleLimit);
|
|
128
|
+
rgba = downscaled.bytes;
|
|
129
|
+
width = downscaled.width;
|
|
130
|
+
height = downscaled.height;
|
|
131
|
+
}
|
|
132
|
+
const limit = opts.maxDimension ?? DEFAULT_MAX_DIMENSION;
|
|
133
|
+
if (width > limit || height > limit) {
|
|
134
|
+
return err(
|
|
135
|
+
imageError({
|
|
136
|
+
code: "image-dimension-out-of-bounds",
|
|
137
|
+
requested: { width, height },
|
|
138
|
+
limit
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return ok({
|
|
143
|
+
bytes: rgba,
|
|
144
|
+
width,
|
|
145
|
+
height,
|
|
146
|
+
mime,
|
|
147
|
+
colorSpace: opts.colorSpace ?? "srgb",
|
|
148
|
+
mipmap: opts.mipmap ?? true
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { parseImage };
|
|
153
|
+
//# sourceMappingURL=parse-image.mjs.map
|
|
154
|
+
//# sourceMappingURL=parse-image.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/resize-image.ts","../src/parse-image.ts"],"names":[],"mappings":"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","file":"parse-image.mjs","sourcesContent":["import type { ImageErrorCode, ImageErrorDetailFor, ImageErrorFor } from '@forgeax/engine-types';\nimport { IMAGE_ERROR_HINTS } from '@forgeax/engine-types';\n\n// Per-code .expected string literals SSOT.\n// Mirrors ASSET_ERROR_HINTS / IMAGE_ERROR_HINTS shape (charter P5 consistent\n// abstraction). AI users surface .expected when a structured error needs to\n// describe the precondition that was violated.\nconst IMAGE_ERROR_EXPECTED: Readonly<Record<ImageErrorCode, string>> = {\n 'image-decode-failed': 'PNG / JPG byte stream decodes successfully',\n 'image-format-unsupported':\n \"mime is one of ['image/png', 'image/jpeg']; uploadTexture format <-> colorSpace family agrees\",\n 'image-dimension-out-of-bounds':\n 'width and height fall under device caps maxTextureDimension2D (or 16384 hard cap)',\n 'image-meta-missing':\n \"<source>.meta.json sidecar (importer: 'image') exists in the same directory\",\n 'image-hdr-decode-failed': 'Radiance RGBE header is valid and pixel data decodes successfully',\n // feat-20260521-sprite-atlas-animation M1 T-03 — vite-plugin-image atlas\n // hook .expected literals (plan-strategy section 2 D-2 + AC-10 a/b/c).\n // ImageErrorImpl construction path is unchanged: the new atlas-* errors\n // flow through `new ImageErrorImpl({ code: 'atlas-...', ...detail })` and\n // pick the .expected string up from this Record at construction time\n // (charter P3 explicit failure SSOT — AI users surface .expected next to\n // .hint after switch (err.code) without parsing the message).\n 'atlas-empty-input': 'images.length >= 1',\n 'atlas-size-exceeded':\n 'image width x height <= maxAtlasSize^2 and each image fits in the atlas footprint',\n 'atlas-region-mismatch': 'sum(regions[i].w x regions[i].h) <= atlasWidth x atlasHeight',\n};\n\n/** Runtime implementation of the correlated `ImageErrorFor<C>` envelope. */\nexport class ImageErrorImpl<C extends ImageErrorCode = ImageErrorCode>\n extends Error\n implements ImageErrorFor<C>\n{\n readonly code: C;\n readonly expected: string;\n readonly hint: string;\n readonly detail: ImageErrorDetailFor<C>;\n\n constructor(detail: ImageErrorDetailFor<C>) {\n const code = detail.code;\n const expected = IMAGE_ERROR_EXPECTED[code];\n const hint = IMAGE_ERROR_HINTS[code];\n super(`[ImageError ${code}] expected: ${expected}; hint: ${hint}`);\n this.name = 'ImageError';\n this.code = code;\n this.expected = expected;\n this.hint = hint;\n this.detail = detail;\n }\n}\n\n/** Construct a structured image error while preserving its code/detail pair. */\nexport function imageError<C extends ImageErrorCode>(\n detail: ImageErrorDetailFor<C>,\n): ImageErrorFor<C> {\n return new ImageErrorImpl<C>(detail);\n}\n\nexport { IMAGE_ERROR_EXPECTED };\n","/** Deterministically downscale tight-packed RGBA pixels with bilinear sampling. */\nexport function downscaleRgba(\n bytes: Uint8Array,\n width: number,\n height: number,\n maxDimension: number,\n): { readonly bytes: Uint8Array; readonly width: number; readonly height: number } {\n if (width <= maxDimension && height <= maxDimension) {\n return { bytes, width, height };\n }\n\n const scale = maxDimension / Math.max(width, height);\n const targetWidth = Math.max(1, Math.round(width * scale));\n const targetHeight = Math.max(1, Math.round(height * scale));\n const output = new Uint8Array(targetWidth * targetHeight * 4);\n const source = (x: number, y: number, channel: number): number =>\n bytes[(y * width + x) * 4 + channel] ?? 0;\n\n for (let y = 0; y < targetHeight; y += 1) {\n const sourceY = ((y + 0.5) * height) / targetHeight - 0.5;\n const y0 = Math.max(0, Math.floor(sourceY));\n const y1 = Math.min(height - 1, y0 + 1);\n const yWeight = Math.max(0, Math.min(1, sourceY - y0));\n for (let x = 0; x < targetWidth; x += 1) {\n const sourceX = ((x + 0.5) * width) / targetWidth - 0.5;\n const x0 = Math.max(0, Math.floor(sourceX));\n const x1 = Math.min(width - 1, x0 + 1);\n const xWeight = Math.max(0, Math.min(1, sourceX - x0));\n const outputOffset = (y * targetWidth + x) * 4;\n for (let channel = 0; channel < 4; channel += 1) {\n const top = source(x0, y0, channel) * (1 - xWeight) + source(x1, y0, channel) * xWeight;\n const bottom = source(x0, y1, channel) * (1 - xWeight) + source(x1, y1, channel) * xWeight;\n output[outputOffset + channel] = Math.round(top * (1 - yWeight) + bottom * yWeight);\n }\n }\n }\n\n return { bytes: output, width: targetWidth, height: targetHeight };\n}\n","import type { DecodedImage, ImageColorSpace, ImageError } from '@forgeax/engine-types';\nimport * as jpeg from 'jpeg-js';\n// Static imports of upng-js + jpeg-js. tsup external keeps both as runtime\n// require() calls so the browser bundle still tree-shakes them out when\n// only image-decoder-browser.ts (createImageBitmap) is reached at runtime.\n// Synchronous decode is required by the parseImage signature contract (see\n// plan-strategy section 3.3).\nimport * as UPNG from 'upng-js';\nimport { imageError } from './errors.js';\nimport { downscaleRgba } from './resize-image.js';\nimport type { Result } from './result.js';\nimport { err, ok } from './result.js';\n\nexport interface ParseImageOptions {\n /**\n * Maximum allowed dimension (width or height). Defaults to 16384 -- the\n * conservative WebGPU `maxTextureDimension2D` floor across desktop +\n * high-end mobile (research F-3 / spec). Test fixtures pass smaller\n * values to exercise the bounds-check code path with tiny fixtures.\n */\n readonly maxDimension?: number;\n\n /** Optional asset-owned cooked-payload target; source bytes remain unchanged. */\n readonly downscaleMaxDimension?: number;\n\n /**\n * Sidecar `colorSpace` carried over to the DecodedImage POD. Defaults to\n * 'srgb' (typical baseColor / albedo path; plan-strategy section 2.5).\n */\n readonly colorSpace?: ImageColorSpace;\n\n /**\n * Sidecar `mipmap` flag carried over to DecodedImage. Defaults to true\n * (auto-generate via runtime mipmap-generator; plan-strategy section 2.6).\n */\n readonly mipmap?: boolean;\n\n /**\n * Optional source path to embed in error.detail.path (decode-failed /\n * format-unsupported variants). decodeImageFromFile fills this in;\n * in-memory parseImage callers leave it empty.\n */\n readonly path?: string;\n}\n\nconst DEFAULT_MAX_DIMENSION = 16384;\n\nconst SUPPORTED_MIMES: readonly string[] = ['image/png', 'image/jpeg'];\n\ninterface UpngDecoded {\n readonly width: number;\n readonly height: number;\n readonly data: Uint8Array;\n}\n\ninterface JpegDecoded {\n readonly width: number;\n readonly height: number;\n readonly data: Uint8Array;\n}\n\n/**\n * Pure synchronous image decoder. Translates raw byte stream + mime hint\n * into a tight-packed RGBA `DecodedImage` POD. Surfaces all four\n * ImageErrorCode members through the structured `Result<DecodedImage,\n * ImageError>` return (charter P3 explicit failure; AGENTS.md \"Return\n * Result, never throw\").\n *\n * Decoder selection follows the mime hint:\n * - `image/png` -> upng-js `UPNG.decode` + `UPNG.toRGBA8`\n * - `image/jpeg` -> jpeg-js `decode` with `formatAsRGBA: true`\n * - other -> `image-format-unsupported`\n *\n * Decoded bytes are always tight-packed RGBA (`bytes.length === width *\n * height * 4`). `colorSpace` and `mipmap` are carried from the supplied\n * `ParseImageOptions` (decodeImageFromFile passes the sidecar settings).\n */\nexport function parseImage(\n bytes: Uint8Array,\n mime: string,\n opts: ParseImageOptions = {},\n): Result<DecodedImage, ImageError> {\n if (!SUPPORTED_MIMES.includes(mime)) {\n return err(\n imageError({\n code: 'image-format-unsupported',\n actualMime: mime,\n ...(opts.path !== undefined ? { path: opts.path } : {}),\n }),\n );\n }\n\n let width = 0;\n let height = 0;\n let rgba: Uint8Array;\n\n try {\n if (mime === 'image/png') {\n const upngMod: {\n decode: (b: Uint8Array | ArrayBuffer) => UpngDecoded;\n toRGBA8: (i: UpngDecoded) => ArrayBuffer[];\n } = ((UPNG as unknown as { default?: typeof UPNG }).default ?? UPNG) as unknown as {\n decode: (b: Uint8Array | ArrayBuffer) => UpngDecoded;\n toRGBA8: (i: UpngDecoded) => ArrayBuffer[];\n };\n const decoded = upngMod.decode(bytes);\n width = decoded.width;\n height = decoded.height;\n const frames = upngMod.toRGBA8(decoded);\n const first = frames[0];\n if (first === undefined) {\n return err(\n imageError({\n code: 'image-decode-failed',\n reason: 'UPNG.toRGBA8 returned no frames',\n ...(opts.path !== undefined ? { path: opts.path } : {}),\n }),\n );\n }\n rgba = new Uint8Array(first);\n } else {\n // image/jpeg\n const jpegMod: {\n decode: (\n b: Uint8Array | ArrayBuffer,\n o?: { useTArray?: boolean; formatAsRGBA?: boolean },\n ) => JpegDecoded;\n } = ((jpeg as unknown as { default?: typeof jpeg }).default ?? jpeg) as unknown as {\n decode: (\n b: Uint8Array | ArrayBuffer,\n o?: { useTArray?: boolean; formatAsRGBA?: boolean },\n ) => JpegDecoded;\n };\n const decoded = jpegMod.decode(bytes, { useTArray: true, formatAsRGBA: true });\n width = decoded.width;\n height = decoded.height;\n rgba = decoded.data;\n }\n } catch (e) {\n return err(\n imageError({\n code: 'image-decode-failed',\n reason: e instanceof Error ? e.message : String(e),\n ...(opts.path !== undefined ? { path: opts.path } : {}),\n }),\n );\n }\n\n const downscaleLimit = opts.downscaleMaxDimension;\n if (downscaleLimit !== undefined && Number.isInteger(downscaleLimit) && downscaleLimit > 0) {\n const downscaled = downscaleRgba(rgba, width, height, downscaleLimit);\n rgba = downscaled.bytes;\n width = downscaled.width;\n height = downscaled.height;\n }\n\n const limit = opts.maxDimension ?? DEFAULT_MAX_DIMENSION;\n if (width > limit || height > limit) {\n return err(\n imageError({\n code: 'image-dimension-out-of-bounds',\n requested: { width, height },\n limit,\n }),\n );\n }\n\n return ok({\n bytes: rgba,\n width,\n height,\n mime: mime as 'image/png' | 'image/jpeg',\n colorSpace: opts.colorSpace ?? 'srgb',\n mipmap: opts.mipmap ?? true,\n });\n}\n"]}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { DecodedImage, ImageColorSpace } from '@forgeax/engine-types';
|
|
2
|
+
/**
|
|
3
|
+
* Existing `*.meta.json` (importer: 'image') sub-asset record (free-form sub-shape; the
|
|
4
|
+
* meta.schema.json $defs/subAsset locks `guid` + `sourceIndex` + `kind` only).
|
|
5
|
+
* Image importer treats the open shape as a key-bearing tuple; the gltf-loader
|
|
6
|
+
* feat works on the same shape.
|
|
7
|
+
*/
|
|
8
|
+
export interface ExistingSubAsset {
|
|
9
|
+
readonly guid: string;
|
|
10
|
+
readonly sourceIndex: number;
|
|
11
|
+
readonly kind: string;
|
|
12
|
+
readonly name?: string;
|
|
13
|
+
readonly sourceKey?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Existing `*.meta.json` (importer: 'image') package record. Mirrors the
|
|
17
|
+
* meta.schema.json $defs/ExternalAssetPackage shape (research F-9). Only
|
|
18
|
+
* the fields consumed by the reimport reuse algorithm are typed here;
|
|
19
|
+
* importSettings stays a free-form open shape.
|
|
20
|
+
*/
|
|
21
|
+
export interface ExistingExternalAssetPackage {
|
|
22
|
+
readonly schemaVersion: string;
|
|
23
|
+
readonly kind: 'external-asset-package';
|
|
24
|
+
readonly importer: 'image';
|
|
25
|
+
readonly source: string;
|
|
26
|
+
readonly importSettings: Readonly<Record<string, unknown>>;
|
|
27
|
+
readonly subAssets: readonly ExistingSubAsset[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Sub-asset record emitted into the regenerated `*.meta.json` (importer: 'image')
|
|
31
|
+
* `subAssets[]` slot. Shape matches `ExistingSubAsset` so two consecutive
|
|
32
|
+
* reimports of byte-identical bytes produce byte-identical JSON
|
|
33
|
+
* (AC-16 byte-stable reimport guarantee).
|
|
34
|
+
*/
|
|
35
|
+
export interface EmittedSubAsset {
|
|
36
|
+
readonly guid: string;
|
|
37
|
+
readonly sourceIndex: number;
|
|
38
|
+
readonly kind: string;
|
|
39
|
+
readonly name?: string;
|
|
40
|
+
readonly sourceKey?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Two-phase reimport reuse algorithm (plan-strategy section 2.2 D-4
|
|
44
|
+
* mirroring the in-flight gltf-loader algorithm).
|
|
45
|
+
*
|
|
46
|
+
* For each fresh sub-asset key the importer would emit, this function:
|
|
47
|
+
* 1. Searches the `existing.subAssets[]` for a `subAssetKeyEqual` hit.
|
|
48
|
+
* 2. On hit -- reuses the existing GUID byte-for-byte.
|
|
49
|
+
* 3. On miss -- mints a fresh UUIDv7 via `AssetGuid.random()`.
|
|
50
|
+
*
|
|
51
|
+
* Image disk schema currently emits a single `kind='texture'` sub-asset per
|
|
52
|
+
* source (cubemap face / array layer reserved for future feat); the
|
|
53
|
+
* algorithm scales to multi-sub-asset sources without modification (charter
|
|
54
|
+
* P5 consistent abstraction).
|
|
55
|
+
*
|
|
56
|
+
* Returns the regenerated `subAssets[]` array. The caller (toAssetPack /
|
|
57
|
+
* runAssetImport) splices it into the `*.meta.json` (importer: 'image') envelope.
|
|
58
|
+
*/
|
|
59
|
+
export declare function reimportReuseMeta(decoded: DecodedImage, existing: ExistingExternalAssetPackage | undefined): readonly EmittedSubAsset[];
|
|
60
|
+
/**
|
|
61
|
+
* HDR colorSpace validator for *.image.meta.json sidecar importSettings
|
|
62
|
+
* (plan-strategy D-8 -- HDR files must use colorSpace=linear).
|
|
63
|
+
*
|
|
64
|
+
* Extension is matched case-insensitively against the HDR set
|
|
65
|
+
* (`.hdr`, `.exr`). When the extension belongs to the HDR set:
|
|
66
|
+
* - `colorSpace='linear'` -> ok, returns `'linear'`
|
|
67
|
+
* - any other colorSpace -> structured rejection with expected/actual
|
|
68
|
+
*
|
|
69
|
+
* Non-HDR extensions pass through the colorSpace unchanged. Downstream
|
|
70
|
+
* callers (decodeImageFromFile, parseImage) invoke this at the
|
|
71
|
+
* sidecar-read entry to fail-fast when an .hdr meta carries srgb.
|
|
72
|
+
*/
|
|
73
|
+
export declare function validateColorSpaceForHdr(ext: string, colorSpace: ImageColorSpace): {
|
|
74
|
+
ok: true;
|
|
75
|
+
value: ImageColorSpace;
|
|
76
|
+
} | {
|
|
77
|
+
ok: false;
|
|
78
|
+
expected: string;
|
|
79
|
+
actual: string;
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=reimport-reuse-meta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reimport-reuse-meta.d.ts","sourceRoot":"","sources":["../src/reimport-reuse-meta.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAM3E;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;GAKG;AACH,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3D,QAAQ,CAAC,SAAS,EAAE,SAAS,gBAAgB,EAAE,CAAC;CACjD;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,YAAY,EACrB,QAAQ,EAAE,4BAA4B,GAAG,SAAS,GACjD,SAAS,eAAe,EAAE,CA0D5B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,eAAe,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,eAAe,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAaxF"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Deterministically downscale tight-packed RGBA pixels with bilinear sampling. */
|
|
2
|
+
export declare function downscaleRgba(bytes: Uint8Array, width: number, height: number, maxDimension: number): {
|
|
3
|
+
readonly bytes: Uint8Array;
|
|
4
|
+
readonly width: number;
|
|
5
|
+
readonly height: number;
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=resize-image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resize-image.d.ts","sourceRoot":"","sources":["../src/resize-image.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,wBAAgB,aAAa,CAC3B,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GACnB;IAAE,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAgCjF"}
|
package/dist/result.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../src/result.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,GAAG,EACH,EAAE,EACF,KAAK,MAAM,EACX,KAAK,SAAS,EACd,KAAK,QAAQ,GACd,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type AssetDecoderContribution, type EquirectAsset, type TextureAsset } from '@forgeax/engine-types';
|
|
2
|
+
export declare const textureContribution: AssetDecoderContribution<TextureAsset, 'texture'>;
|
|
3
|
+
export declare const equirectContribution: AssetDecoderContribution<EquirectAsset, 'equirect'>;
|
|
4
|
+
//# sourceMappingURL=asset-decoders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-decoders.d.ts","sourceRoot":"","sources":["../../src/runtime/asset-decoders.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,wBAAwB,EAK7B,KAAK,aAAa,EAGlB,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAkH/B,eAAO,MAAM,mBAAmB,EAAE,wBAAwB,CAAC,YAAY,EAAE,SAAS,CAYjF,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,wBAAwB,CAAC,aAAa,EAAE,UAAU,CAYpF,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive the semantic identity for an image producer output.
|
|
3
|
+
*
|
|
4
|
+
* The role is the producer-owned identity. Source path and sourceIndex are
|
|
5
|
+
* locators only and must not be folded into this key.
|
|
6
|
+
*/
|
|
7
|
+
export interface ImageSourceKeyLocator {
|
|
8
|
+
readonly sourcePath?: string;
|
|
9
|
+
readonly sourceIndex?: number;
|
|
10
|
+
}
|
|
11
|
+
export type ImageSourceKeyResult = {
|
|
12
|
+
readonly ok: true;
|
|
13
|
+
readonly keys: readonly string[];
|
|
14
|
+
} | {
|
|
15
|
+
readonly ok: false;
|
|
16
|
+
readonly code: 'missing-source-key' | 'duplicate-source-key';
|
|
17
|
+
readonly roles: readonly string[];
|
|
18
|
+
};
|
|
19
|
+
export declare function deriveImageSourceKey(role: string, _locator?: ImageSourceKeyLocator): string | undefined;
|
|
20
|
+
export declare function deriveImageSourceKeys(roles: readonly string[]): ImageSourceKeyResult;
|
|
21
|
+
//# sourceMappingURL=source-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-key.d.ts","sourceRoot":"","sources":["../src/source-key.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GACvD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,IAAI,EAAE,oBAAoB,GAAG,sBAAsB,CAAC;IAC7D,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC,CAAC;AAEN,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,qBAAqB,GAC/B,MAAM,GAAG,SAAS,CAIpB;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,oBAAoB,CAYpF"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-asset-key shape mirroring the in-flight gltf-loader feat (plan-strategy
|
|
3
|
+
* section 2.2 D-4 same-shape). Each sub-asset emitted into a `*.meta.json`
|
|
4
|
+
* sidecar (uniformly `<source>.meta.json` across image / gltf arms,
|
|
5
|
+
* dispatched on top-level `importer` field per feat-20260521) carries a
|
|
6
|
+
* `{kind, name?, indexFallback}` triple so the importer can deterministically
|
|
7
|
+
* match an existing sub-asset across reimports (charter P5 producer/consumer
|
|
8
|
+
* split + AC-14 cross-feat alignment).
|
|
9
|
+
*
|
|
10
|
+
* The image disk schema is currently single-sub-asset; this file collapses
|
|
11
|
+
* to `kind='texture'` + `indexFallback='textures/0'` so the same matching
|
|
12
|
+
* algorithm is reusable when the future cubemap / array-layer feat lands
|
|
13
|
+
* (plan-strategy R5 free-form schema).
|
|
14
|
+
*/
|
|
15
|
+
export interface SubAssetKey {
|
|
16
|
+
/** Discriminator literal -- 'texture' for engine-image; 'mesh' / 'material' / 'scene' / 'image' for gltf-loader. */
|
|
17
|
+
readonly kind: string;
|
|
18
|
+
/** Optional human-readable name field; absent when the source has no symbolic identifier. */
|
|
19
|
+
readonly name?: string;
|
|
20
|
+
/** Path-style fallback identifier (e.g. 'images/0' / 'meshes/0'); always non-empty. */
|
|
21
|
+
readonly indexFallback: string;
|
|
22
|
+
}
|
|
23
|
+
export interface SubAssetKeyInput {
|
|
24
|
+
readonly kind: string;
|
|
25
|
+
readonly sourceIndex: number;
|
|
26
|
+
readonly name?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Construct a sub-asset key from importer input. The `indexFallback` is
|
|
30
|
+
* synthesised from the kind literal + sourceIndex so two importers
|
|
31
|
+
* (image / gltf-loader) emit identical fallback strings for the same
|
|
32
|
+
* `(kind, sourceIndex)` pair (AC-14).
|
|
33
|
+
*/
|
|
34
|
+
export declare function subAssetKey(input: SubAssetKeyInput): SubAssetKey;
|
|
35
|
+
/**
|
|
36
|
+
* Two-phase equality predicate used by reimportReuseMeta for GUID
|
|
37
|
+
* preservation across reimports:
|
|
38
|
+
*
|
|
39
|
+
* Phase 1 -- (kind + name + idx) full match: identical in all three fields
|
|
40
|
+
* Phase 2 -- (kind + idx) match: same kind + indexFallback, name absent or
|
|
41
|
+
* equal (collapses when both sides omit name)
|
|
42
|
+
* Phase 3 -- otherwise: false (importer mints fresh UUIDv7)
|
|
43
|
+
*
|
|
44
|
+
* The two phases are folded into a single boolean predicate here; the
|
|
45
|
+
* reimportReuseMeta caller iterates the existing subAssets list and picks
|
|
46
|
+
* the first hit (deterministic order; matches AC-16 byte-identical
|
|
47
|
+
* reimport).
|
|
48
|
+
*/
|
|
49
|
+
export declare function subAssetKeyEqual(a: SubAssetKey, b: SubAssetKey): boolean;
|
|
50
|
+
//# sourceMappingURL=sub-asset-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sub-asset-key.d.ts","sourceRoot":"","sources":["../src/sub-asset-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,WAAW;IAC1B,oHAAoH;IACpH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,uFAAuF;IACvF,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,WAAW,CAMhE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,GAAG,OAAO,CAQxE"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { AssetCompression, DecodedImage, ImageMeta } from '@forgeax/engine-types';
|
|
2
|
+
/**
|
|
3
|
+
* External-asset-package envelope shape mirroring the
|
|
4
|
+
* `packages/pack/schema/meta.schema.json` $defs/ExternalAssetPackage.
|
|
5
|
+
* Consumed by `forgeax-engine-remote-asset import` (CLI entry, M3b) +
|
|
6
|
+
* the image importer in M2 build-time path. The internal-text-package
|
|
7
|
+
* variant ($defs/InternalTextPackage) is owned by the gltf-loader feat.
|
|
8
|
+
*/
|
|
9
|
+
export interface ExternalAssetPackage {
|
|
10
|
+
readonly schemaVersion: string;
|
|
11
|
+
readonly kind: 'external-asset-package';
|
|
12
|
+
readonly importer: 'image';
|
|
13
|
+
readonly source: string;
|
|
14
|
+
readonly importSettings: ImageImportSettings;
|
|
15
|
+
readonly subAssets: readonly ExternalSubAsset[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Free-form image importer settings persisted into `*.meta.json` (importer: 'image')
|
|
19
|
+
* `importSettings`. Matches `ImageMeta` POD field-for-field (charter P5
|
|
20
|
+
* single SSOT) but the open Record shape allows future minor adds (cubemap
|
|
21
|
+
* face / array layer / custom colorSpace) without breaking existing meta
|
|
22
|
+
* files (plan-strategy R5).
|
|
23
|
+
*/
|
|
24
|
+
export interface ImageImportSettings extends Readonly<Record<string, unknown>> {
|
|
25
|
+
readonly colorSpace: ImageMeta['colorSpace'];
|
|
26
|
+
readonly mipmap: ImageMeta['mipmap'];
|
|
27
|
+
readonly addressMode: ImageMeta['addressMode'];
|
|
28
|
+
readonly filterMode: ImageMeta['filterMode'];
|
|
29
|
+
readonly downscaleMaxDimension?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Explicit per-asset compression override (AC-01: importSettings carries
|
|
32
|
+
* compression intent). When omitted, the build-time compression strategy
|
|
33
|
+
* table decides by artifact kind (mesh -> zstd, texture -> none). When set,
|
|
34
|
+
* this value wins over the default table for this asset.
|
|
35
|
+
*/
|
|
36
|
+
readonly compression?: AssetCompression;
|
|
37
|
+
}
|
|
38
|
+
export interface ExternalSubAsset {
|
|
39
|
+
readonly guid: string;
|
|
40
|
+
readonly sourceIndex: number;
|
|
41
|
+
readonly kind: string;
|
|
42
|
+
readonly name?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Pure function that translates a DecodedImage POD + ImageMeta POD into
|
|
46
|
+
* an external-asset-package envelope ready to be JSON-stringified into a
|
|
47
|
+
* `*.meta.json` sidecar with importer: 'image' (plan-strategy section 3.2 sequence A; AC-13
|
|
48
|
+
* disk schema reuse).
|
|
49
|
+
*
|
|
50
|
+
* The single sub-asset emitted carries:
|
|
51
|
+
* - `guid` -- copied verbatim from `meta.guid` (the producer guarantees
|
|
52
|
+
* the GUID is freshly minted or reused via `reimportReuseMeta`)
|
|
53
|
+
* - `sourceIndex: 0` (single-sub-asset shape; cubemap / array reserved
|
|
54
|
+
* for future feat)
|
|
55
|
+
* - `kind: 'texture'` (closed literal for image-importer emit)
|
|
56
|
+
*
|
|
57
|
+
* Two consecutive calls with identical inputs produce JSON.stringify
|
|
58
|
+
* byte-equal output (AC-16 idempotent reimport).
|
|
59
|
+
*/
|
|
60
|
+
export declare function toAssetPack(decoded: DecodedImage, meta: ImageMeta): ExternalAssetPackage;
|
|
61
|
+
//# sourceMappingURL=to-asset-pack.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-asset-pack.d.ts","sourceRoot":"","sources":["../src/to-asset-pack.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEvF;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,mBAAmB,CAAC;IAC7C,QAAQ,CAAC,SAAS,EAAE,SAAS,gBAAgB,EAAE,CAAC;CACjD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAoB,SAAQ,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC,YAAY,CAAC,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IACrC,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,aAAa,CAAC,CAAC;IAC/C,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC,YAAY,CAAC,CAAC;IAC7C,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,gBAAgB,CAAC;CACzC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,GAAG,oBAAoB,CA+BxF"}
|