@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":"encode-source-too-large.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/encode-source-too-large.unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/errors.test-d.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-importer-conversion-failure.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/image-importer-conversion-failure.unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-importer-hdr-equirect.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/image-importer-hdr-equirect.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-importer-topology.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/image-importer-topology.unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-importer.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/image-importer.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-local-artifacts.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/image-local-artifacts.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/image.unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ktx2-basis-importer.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/ktx2-basis-importer.unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Solid-colour 8-bit RGBA PNG, standard zlib/deflate encoding. */
|
|
2
|
+
export declare function makePng(width: number, height: number, rgba: [number, number, number, number]): Uint8Array;
|
|
3
|
+
/** Solid-colour RGBA JPEG of the given dimensions. */
|
|
4
|
+
export declare function makeJpg(width: number, height: number, rgba: [number, number, number, number]): Uint8Array;
|
|
5
|
+
/**
|
|
6
|
+
* A valid PNG header + IHDR followed by an IDAT whose deflate stream is
|
|
7
|
+
* garbage, so a real PNG decoder fails (image-decode-failed). Built from the
|
|
8
|
+
* same chunk machinery as makePng, with a corrupted IDAT body — replaces the
|
|
9
|
+
* former committed corrupt.png fixture.
|
|
10
|
+
*/
|
|
11
|
+
export declare function makeCorruptPng(): Uint8Array;
|
|
12
|
+
//# sourceMappingURL=make-fixture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"make-fixture.d.ts","sourceRoot":"","sources":["../../src/__tests__/make-fixture.ts"],"names":[],"mappings":"AAsCA,mEAAmE;AACnE,wBAAgB,OAAO,CACrB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GACrC,UAAU,CAsBZ;AAED,sDAAsD;AACtD,wBAAgB,OAAO,CACrB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GACrC,UAAU,CAUZ;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,UAAU,CAS3C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-image-downscale.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/parse-image-downscale.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-key.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/source-key.unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DecodedImage, ImageError, ImageMeta } from '@forgeax/engine-types';
|
|
2
|
+
import { type HdrDecoded } from './hdr-decoder.js';
|
|
3
|
+
import type { Result } from './result.js';
|
|
4
|
+
/**
|
|
5
|
+
* Result envelope returned by decodeImageFromFile for PNG/JPG sources.
|
|
6
|
+
* Carries both the raw DecodedImage POD (for direct uploadTexture consumption)
|
|
7
|
+
* and the parsed ImageMeta POD.
|
|
8
|
+
*/
|
|
9
|
+
export interface DecodedImageWithMeta {
|
|
10
|
+
readonly decoded: DecodedImage;
|
|
11
|
+
readonly meta: ImageMeta;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Result envelope returned by decodeImageFromFile for .hdr sources.
|
|
15
|
+
* Carries the HDR-decoded float data ready for cubemap upload.
|
|
16
|
+
*/
|
|
17
|
+
export interface DecodedHdrWithMeta {
|
|
18
|
+
readonly hdr: HdrDecoded;
|
|
19
|
+
readonly meta: ImageMeta;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Async file-system entry to the image importer (plan-strategy section 3.2
|
|
23
|
+
* sequence A; AC-17 path (a) sidecar three-way fallback).
|
|
24
|
+
*
|
|
25
|
+
* Behaviour (left-to-right, fail-fast on first surfaced ImageError):
|
|
26
|
+
* 1. Sniff extension -- not in `.png / .jpg / .jpeg` -> image-format-unsupported
|
|
27
|
+
* 2. Stat source -- absent -> image-decode-failed with the source path
|
|
28
|
+
* 3. Stat sibling `<source>.meta.json` (importer: 'image') -- absent -> image-meta-missing
|
|
29
|
+
* 4. Read source bytes + parse sidecar JSON
|
|
30
|
+
* 5. Hand off to `parseImage(bytes, mime, opts)` -- surfaces image-decode-failed
|
|
31
|
+
* / image-format-unsupported / image-dimension-out-of-bounds
|
|
32
|
+
* 6. Compose DecodedImage POD + ImageMeta POD return envelope.
|
|
33
|
+
*
|
|
34
|
+
* AC-17 path (a) lock: when the sidecar is absent, the returned error
|
|
35
|
+
* carries `detail.sourcePath` + `detail.expectedSidecarPath` so AI users
|
|
36
|
+
* read .hint and run `forgeax-engine-remote-asset import <path>` to
|
|
37
|
+
* self-recover (charter P3 explicit failure + IDE jump-to-source).
|
|
38
|
+
*/
|
|
39
|
+
export declare function decodeImageFromFile(sourcePath: string): Promise<Result<DecodedImageWithMeta, ImageError>>;
|
|
40
|
+
//# sourceMappingURL=decode-image-from-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decode-image-from-file.d.ts","sourceRoot":"","sources":["../src/decode-image-from-file.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEjF,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AA2B1C;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;CAC1B;AAkBD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC,CA+InD"}
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { stat, readFile } from 'fs/promises';
|
|
2
|
+
import { extname, dirname, join } from 'path';
|
|
3
|
+
import { err, ok, IMAGE_ERROR_HINTS } from '@forgeax/engine-types';
|
|
4
|
+
import * as jpeg from 'jpeg-js';
|
|
5
|
+
import * as UPNG from 'upng-js';
|
|
6
|
+
|
|
7
|
+
// src/decode-image-from-file.ts
|
|
8
|
+
var IMAGE_ERROR_EXPECTED = {
|
|
9
|
+
"image-decode-failed": "PNG / JPG byte stream decodes successfully",
|
|
10
|
+
"image-format-unsupported": "mime is one of ['image/png', 'image/jpeg']; uploadTexture format <-> colorSpace family agrees",
|
|
11
|
+
"image-dimension-out-of-bounds": "width and height fall under device caps maxTextureDimension2D (or 16384 hard cap)",
|
|
12
|
+
"image-meta-missing": "<source>.meta.json sidecar (importer: 'image') exists in the same directory",
|
|
13
|
+
"image-hdr-decode-failed": "Radiance RGBE header is valid and pixel data decodes successfully",
|
|
14
|
+
// feat-20260521-sprite-atlas-animation M1 T-03 — vite-plugin-image atlas
|
|
15
|
+
// hook .expected literals (plan-strategy section 2 D-2 + AC-10 a/b/c).
|
|
16
|
+
// ImageErrorImpl construction path is unchanged: the new atlas-* errors
|
|
17
|
+
// flow through `new ImageErrorImpl({ code: 'atlas-...', ...detail })` and
|
|
18
|
+
// pick the .expected string up from this Record at construction time
|
|
19
|
+
// (charter P3 explicit failure SSOT — AI users surface .expected next to
|
|
20
|
+
// .hint after switch (err.code) without parsing the message).
|
|
21
|
+
"atlas-empty-input": "images.length >= 1",
|
|
22
|
+
"atlas-size-exceeded": "image width x height <= maxAtlasSize^2 and each image fits in the atlas footprint",
|
|
23
|
+
"atlas-region-mismatch": "sum(regions[i].w x regions[i].h) <= atlasWidth x atlasHeight"
|
|
24
|
+
};
|
|
25
|
+
var ImageErrorImpl = class extends Error {
|
|
26
|
+
code;
|
|
27
|
+
expected;
|
|
28
|
+
hint;
|
|
29
|
+
detail;
|
|
30
|
+
constructor(detail) {
|
|
31
|
+
const code = detail.code;
|
|
32
|
+
const expected = IMAGE_ERROR_EXPECTED[code];
|
|
33
|
+
const hint = IMAGE_ERROR_HINTS[code];
|
|
34
|
+
super(`[ImageError ${code}] expected: ${expected}; hint: ${hint}`);
|
|
35
|
+
this.name = "ImageError";
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.expected = expected;
|
|
38
|
+
this.hint = hint;
|
|
39
|
+
this.detail = detail;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
function imageError(detail) {
|
|
43
|
+
return new ImageErrorImpl(detail);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/hdr-decoder.ts
|
|
47
|
+
var LF = 10;
|
|
48
|
+
function decodeHdr(bytes) {
|
|
49
|
+
let pos = 0;
|
|
50
|
+
if (bytes.length < 11) {
|
|
51
|
+
return err(hdrDecodeError("file too short for Radiance HDR header"));
|
|
52
|
+
}
|
|
53
|
+
const magic = String.fromCharCode(...bytes.subarray(0, 11));
|
|
54
|
+
if (magic !== "#?RADIANCE\n" && magic !== "#?RGBE\n") {
|
|
55
|
+
return err(
|
|
56
|
+
hdrDecodeError("missing or invalid Radiance HDR magic; expected #?RADIANCE or #?RGBE")
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
pos = 11;
|
|
60
|
+
let formatFound = false;
|
|
61
|
+
while (pos < bytes.length) {
|
|
62
|
+
const lineEnd = findByte(bytes, LF, pos);
|
|
63
|
+
if (lineEnd === -1) return err(hdrDecodeError("header truncated before empty line"));
|
|
64
|
+
const line = asciiSubstring(bytes, pos, lineEnd);
|
|
65
|
+
pos = lineEnd + 1;
|
|
66
|
+
if (line === "") break;
|
|
67
|
+
if (line.startsWith("FORMAT=")) {
|
|
68
|
+
const val = line.slice(7);
|
|
69
|
+
if (val === "32-bit_rle_rgbe") {
|
|
70
|
+
formatFound = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!formatFound) {
|
|
75
|
+
return err(hdrDecodeError("missing or unsupported FORMAT; expected FORMAT=32-bit_rle_rgbe"));
|
|
76
|
+
}
|
|
77
|
+
if (pos >= bytes.length) return err(hdrDecodeError("missing resolution line"));
|
|
78
|
+
const resLineEnd = findByte(bytes, LF, pos);
|
|
79
|
+
if (resLineEnd === -1) return err(hdrDecodeError("resolution line truncated"));
|
|
80
|
+
const resLine = asciiSubstring(bytes, pos, resLineEnd);
|
|
81
|
+
pos = resLineEnd + 1;
|
|
82
|
+
const resMatch = /^-Y\s+(\d+)\s+\+X\s+(\d+)\s*$/.exec(resLine);
|
|
83
|
+
if (resMatch === null) {
|
|
84
|
+
return err(
|
|
85
|
+
hdrDecodeError(`unexpected resolution line format: "${resLine}"; expected "-Y H +X W"`)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const height = Number(resMatch[1]);
|
|
89
|
+
const width = Number(resMatch[2]);
|
|
90
|
+
if (height === void 0 || width === void 0 || height <= 0 || width <= 0) {
|
|
91
|
+
return err(hdrDecodeError(`invalid dimensions: ${width}x${height}`));
|
|
92
|
+
}
|
|
93
|
+
const pixelCount = width * height;
|
|
94
|
+
const rgbe = new Uint8Array(pixelCount * 4);
|
|
95
|
+
const scanlineBytes = width * 4;
|
|
96
|
+
for (let y = 0; y < height; y++) {
|
|
97
|
+
if (pos + 4 > bytes.length) {
|
|
98
|
+
return err(hdrDecodeError(`truncated at scanline ${y}: expected 4-byte prefix`));
|
|
99
|
+
}
|
|
100
|
+
const p0 = bytes[pos];
|
|
101
|
+
const p1 = bytes[pos + 1];
|
|
102
|
+
const pHi = bytes[pos + 2];
|
|
103
|
+
const pLo = bytes[pos + 3];
|
|
104
|
+
if (p0 === void 0 || p1 === void 0 || pHi === void 0 || pLo === void 0) {
|
|
105
|
+
return err(hdrDecodeError(`truncated at scanline ${y}`));
|
|
106
|
+
}
|
|
107
|
+
const prefixWidth = pHi << 8 | pLo;
|
|
108
|
+
if (p0 === 2 && p1 === 2 && prefixWidth === width && width >= 8 && width <= 32767) {
|
|
109
|
+
const decoded = decodeNewRleScanline(bytes, pos + 4, width);
|
|
110
|
+
if (decoded === null) {
|
|
111
|
+
return err(hdrDecodeError(`RLE decode failed at scanline ${y}`));
|
|
112
|
+
}
|
|
113
|
+
const base = y * scanlineBytes;
|
|
114
|
+
const channels = [decoded[0], decoded[1], decoded[2], decoded[3]];
|
|
115
|
+
for (let x = 0; x < width; x++) {
|
|
116
|
+
for (let ch = 0; ch < 4; ch++) {
|
|
117
|
+
rgbe[base + x * 4 + ch] = channels[ch][x];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
pos += 4 + decoded.totalBytes;
|
|
121
|
+
} else {
|
|
122
|
+
return err(
|
|
123
|
+
hdrDecodeError(
|
|
124
|
+
"old-RLE format is not supported; only new-RLE (32-bit_rle_rgbe) is accepted"
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const out = new Float32Array(pixelCount * 4);
|
|
130
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
131
|
+
const base = i * 4;
|
|
132
|
+
const r = rgbe[base];
|
|
133
|
+
const g = rgbe[base + 1];
|
|
134
|
+
const b = rgbe[base + 2];
|
|
135
|
+
const e = rgbe[base + 3];
|
|
136
|
+
out[base + 3] = 1;
|
|
137
|
+
if (e === 0) {
|
|
138
|
+
out[base] = 0;
|
|
139
|
+
out[base + 1] = 0;
|
|
140
|
+
out[base + 2] = 0;
|
|
141
|
+
} else {
|
|
142
|
+
const f = 2 ** (e - 136);
|
|
143
|
+
out[base] = (r + 0.5) * f;
|
|
144
|
+
out[base + 1] = (g + 0.5) * f;
|
|
145
|
+
out[base + 2] = (b + 0.5) * f;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return ok({ width, height, data: out });
|
|
149
|
+
}
|
|
150
|
+
function decodeNewRleScanline(bytes, start, width) {
|
|
151
|
+
const channels = [];
|
|
152
|
+
let cursor = start;
|
|
153
|
+
for (let ch = 0; ch < 4; ch++) {
|
|
154
|
+
const data = new Uint8Array(width);
|
|
155
|
+
let x = 0;
|
|
156
|
+
while (x < width) {
|
|
157
|
+
if (cursor >= bytes.length) return null;
|
|
158
|
+
const code = bytes[cursor];
|
|
159
|
+
cursor++;
|
|
160
|
+
if (code > 128) {
|
|
161
|
+
const count = code - 128;
|
|
162
|
+
if (cursor >= bytes.length || x + count > width) return null;
|
|
163
|
+
const val = bytes[cursor];
|
|
164
|
+
cursor++;
|
|
165
|
+
data.fill(val, x, x + count);
|
|
166
|
+
x += count;
|
|
167
|
+
} else {
|
|
168
|
+
if (code === 0) continue;
|
|
169
|
+
if (cursor + code > bytes.length || x + code > width) return null;
|
|
170
|
+
for (let k = 0; k < code; k++) {
|
|
171
|
+
data[x + k] = bytes[cursor + k];
|
|
172
|
+
}
|
|
173
|
+
cursor += code;
|
|
174
|
+
x += code;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
channels.push(data);
|
|
178
|
+
}
|
|
179
|
+
const ch0 = channels[0];
|
|
180
|
+
const ch1 = channels[1];
|
|
181
|
+
const ch2 = channels[2];
|
|
182
|
+
const ch3 = channels[3];
|
|
183
|
+
if (ch0 === void 0 || ch1 === void 0 || ch2 === void 0 || ch3 === void 0) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
0: ch0,
|
|
188
|
+
1: ch1,
|
|
189
|
+
2: ch2,
|
|
190
|
+
3: ch3,
|
|
191
|
+
totalBytes: cursor - start
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function findByte(bytes, target, start) {
|
|
195
|
+
for (let i = start; i < bytes.length; i++) {
|
|
196
|
+
if (bytes[i] === target) return i;
|
|
197
|
+
}
|
|
198
|
+
return -1;
|
|
199
|
+
}
|
|
200
|
+
function asciiSubstring(bytes, start, end) {
|
|
201
|
+
let s = "";
|
|
202
|
+
for (let i = start; i < end; i++) {
|
|
203
|
+
s += String.fromCharCode(bytes[i]);
|
|
204
|
+
}
|
|
205
|
+
return s;
|
|
206
|
+
}
|
|
207
|
+
function hdrDecodeError(reason) {
|
|
208
|
+
return imageError({
|
|
209
|
+
code: "image-hdr-decode-failed",
|
|
210
|
+
reason
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/resize-image.ts
|
|
215
|
+
function downscaleRgba(bytes, width, height, maxDimension) {
|
|
216
|
+
if (width <= maxDimension && height <= maxDimension) {
|
|
217
|
+
return { bytes, width, height };
|
|
218
|
+
}
|
|
219
|
+
const scale = maxDimension / Math.max(width, height);
|
|
220
|
+
const targetWidth = Math.max(1, Math.round(width * scale));
|
|
221
|
+
const targetHeight = Math.max(1, Math.round(height * scale));
|
|
222
|
+
const output = new Uint8Array(targetWidth * targetHeight * 4);
|
|
223
|
+
const source = (x, y, channel) => bytes[(y * width + x) * 4 + channel] ?? 0;
|
|
224
|
+
for (let y = 0; y < targetHeight; y += 1) {
|
|
225
|
+
const sourceY = (y + 0.5) * height / targetHeight - 0.5;
|
|
226
|
+
const y0 = Math.max(0, Math.floor(sourceY));
|
|
227
|
+
const y1 = Math.min(height - 1, y0 + 1);
|
|
228
|
+
const yWeight = Math.max(0, Math.min(1, sourceY - y0));
|
|
229
|
+
for (let x = 0; x < targetWidth; x += 1) {
|
|
230
|
+
const sourceX = (x + 0.5) * width / targetWidth - 0.5;
|
|
231
|
+
const x0 = Math.max(0, Math.floor(sourceX));
|
|
232
|
+
const x1 = Math.min(width - 1, x0 + 1);
|
|
233
|
+
const xWeight = Math.max(0, Math.min(1, sourceX - x0));
|
|
234
|
+
const outputOffset = (y * targetWidth + x) * 4;
|
|
235
|
+
for (let channel = 0; channel < 4; channel += 1) {
|
|
236
|
+
const top = source(x0, y0, channel) * (1 - xWeight) + source(x1, y0, channel) * xWeight;
|
|
237
|
+
const bottom = source(x0, y1, channel) * (1 - xWeight) + source(x1, y1, channel) * xWeight;
|
|
238
|
+
output[outputOffset + channel] = Math.round(top * (1 - yWeight) + bottom * yWeight);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { bytes: output, width: targetWidth, height: targetHeight };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/parse-image.ts
|
|
246
|
+
var DEFAULT_MAX_DIMENSION = 16384;
|
|
247
|
+
var SUPPORTED_MIMES = ["image/png", "image/jpeg"];
|
|
248
|
+
function parseImage(bytes, mime, opts = {}) {
|
|
249
|
+
if (!SUPPORTED_MIMES.includes(mime)) {
|
|
250
|
+
return err(
|
|
251
|
+
imageError({
|
|
252
|
+
code: "image-format-unsupported",
|
|
253
|
+
actualMime: mime,
|
|
254
|
+
...opts.path !== void 0 ? { path: opts.path } : {}
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
let width = 0;
|
|
259
|
+
let height = 0;
|
|
260
|
+
let rgba;
|
|
261
|
+
try {
|
|
262
|
+
if (mime === "image/png") {
|
|
263
|
+
const upngMod = UPNG.default ?? UPNG;
|
|
264
|
+
const decoded = upngMod.decode(bytes);
|
|
265
|
+
width = decoded.width;
|
|
266
|
+
height = decoded.height;
|
|
267
|
+
const frames = upngMod.toRGBA8(decoded);
|
|
268
|
+
const first = frames[0];
|
|
269
|
+
if (first === void 0) {
|
|
270
|
+
return err(
|
|
271
|
+
imageError({
|
|
272
|
+
code: "image-decode-failed",
|
|
273
|
+
reason: "UPNG.toRGBA8 returned no frames",
|
|
274
|
+
...opts.path !== void 0 ? { path: opts.path } : {}
|
|
275
|
+
})
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
rgba = new Uint8Array(first);
|
|
279
|
+
} else {
|
|
280
|
+
const jpegMod = jpeg.default ?? jpeg;
|
|
281
|
+
const decoded = jpegMod.decode(bytes, { useTArray: true, formatAsRGBA: true });
|
|
282
|
+
width = decoded.width;
|
|
283
|
+
height = decoded.height;
|
|
284
|
+
rgba = decoded.data;
|
|
285
|
+
}
|
|
286
|
+
} catch (e) {
|
|
287
|
+
return err(
|
|
288
|
+
imageError({
|
|
289
|
+
code: "image-decode-failed",
|
|
290
|
+
reason: e instanceof Error ? e.message : String(e),
|
|
291
|
+
...opts.path !== void 0 ? { path: opts.path } : {}
|
|
292
|
+
})
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const downscaleLimit = opts.downscaleMaxDimension;
|
|
296
|
+
if (downscaleLimit !== void 0 && Number.isInteger(downscaleLimit) && downscaleLimit > 0) {
|
|
297
|
+
const downscaled = downscaleRgba(rgba, width, height, downscaleLimit);
|
|
298
|
+
rgba = downscaled.bytes;
|
|
299
|
+
width = downscaled.width;
|
|
300
|
+
height = downscaled.height;
|
|
301
|
+
}
|
|
302
|
+
const limit = opts.maxDimension ?? DEFAULT_MAX_DIMENSION;
|
|
303
|
+
if (width > limit || height > limit) {
|
|
304
|
+
return err(
|
|
305
|
+
imageError({
|
|
306
|
+
code: "image-dimension-out-of-bounds",
|
|
307
|
+
requested: { width, height },
|
|
308
|
+
limit
|
|
309
|
+
})
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return ok({
|
|
313
|
+
bytes: rgba,
|
|
314
|
+
width,
|
|
315
|
+
height,
|
|
316
|
+
mime,
|
|
317
|
+
colorSpace: opts.colorSpace ?? "srgb",
|
|
318
|
+
mipmap: opts.mipmap ?? true
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/decode-image-from-file.ts
|
|
323
|
+
var EXT_TO_MIME = {
|
|
324
|
+
".png": "image/png",
|
|
325
|
+
".jpg": "image/jpeg",
|
|
326
|
+
".jpeg": "image/jpeg",
|
|
327
|
+
".hdr": "image/vnd.radiance"
|
|
328
|
+
};
|
|
329
|
+
function deriveSidecarPath(sourcePath) {
|
|
330
|
+
const dir = dirname(sourcePath);
|
|
331
|
+
const base = sourcePath.slice(dir.length + 1);
|
|
332
|
+
return join(dir, `${base}.meta.json`);
|
|
333
|
+
}
|
|
334
|
+
async function decodeImageFromFile(sourcePath) {
|
|
335
|
+
const ext = extname(sourcePath).toLowerCase();
|
|
336
|
+
const mime = EXT_TO_MIME[ext];
|
|
337
|
+
if (mime === void 0) {
|
|
338
|
+
return err(
|
|
339
|
+
imageError({
|
|
340
|
+
code: "image-format-unsupported",
|
|
341
|
+
actualMime: `extension '${ext || "<none>"}'`,
|
|
342
|
+
path: sourcePath
|
|
343
|
+
})
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
const sidecarPath = deriveSidecarPath(sourcePath);
|
|
347
|
+
try {
|
|
348
|
+
await stat(sourcePath);
|
|
349
|
+
} catch (e) {
|
|
350
|
+
return err(
|
|
351
|
+
imageError({
|
|
352
|
+
code: "image-decode-failed",
|
|
353
|
+
reason: `failed to read source: ${e instanceof Error ? e.message : String(e)}`,
|
|
354
|
+
path: sourcePath
|
|
355
|
+
})
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
await stat(sidecarPath);
|
|
360
|
+
} catch {
|
|
361
|
+
return err(
|
|
362
|
+
imageError({
|
|
363
|
+
code: "image-meta-missing",
|
|
364
|
+
sourcePath,
|
|
365
|
+
expectedSidecarPath: sidecarPath
|
|
366
|
+
})
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
let sidecarText;
|
|
370
|
+
try {
|
|
371
|
+
sidecarText = (await readFile(sidecarPath)).toString("utf8");
|
|
372
|
+
} catch (e) {
|
|
373
|
+
return err(
|
|
374
|
+
imageError({
|
|
375
|
+
code: "image-decode-failed",
|
|
376
|
+
reason: `failed to read sidecar: ${e instanceof Error ? e.message : String(e)}`,
|
|
377
|
+
path: sidecarPath
|
|
378
|
+
})
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
let sidecar;
|
|
382
|
+
try {
|
|
383
|
+
sidecar = JSON.parse(sidecarText);
|
|
384
|
+
} catch (e) {
|
|
385
|
+
return err(
|
|
386
|
+
imageError({
|
|
387
|
+
code: "image-decode-failed",
|
|
388
|
+
reason: `sidecar JSON parse failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
389
|
+
path: sidecarPath
|
|
390
|
+
})
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
const settings = sidecar.importSettings ?? {};
|
|
394
|
+
const colorSpace = settings.colorSpace ?? "srgb";
|
|
395
|
+
const mipmap = settings.mipmap ?? "auto";
|
|
396
|
+
const addressMode = settings.addressMode ?? "repeat";
|
|
397
|
+
const filterMode = settings.filterMode ?? "linear";
|
|
398
|
+
const guid = sidecar.subAssets[0]?.guid ?? "";
|
|
399
|
+
const meta = {
|
|
400
|
+
guid,
|
|
401
|
+
colorSpace,
|
|
402
|
+
mipmap,
|
|
403
|
+
addressMode,
|
|
404
|
+
filterMode
|
|
405
|
+
};
|
|
406
|
+
let bytes;
|
|
407
|
+
try {
|
|
408
|
+
const buf = await readFile(sourcePath);
|
|
409
|
+
bytes = new Uint8Array(buf);
|
|
410
|
+
} catch (e) {
|
|
411
|
+
return err(
|
|
412
|
+
imageError({
|
|
413
|
+
code: "image-decode-failed",
|
|
414
|
+
reason: `failed to read source: ${e instanceof Error ? e.message : String(e)}`,
|
|
415
|
+
path: sourcePath
|
|
416
|
+
})
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
if (mime === "image/vnd.radiance") {
|
|
420
|
+
if (colorSpace !== "linear") {
|
|
421
|
+
return err(
|
|
422
|
+
imageError({
|
|
423
|
+
code: "image-format-unsupported",
|
|
424
|
+
actualMime: `colorSpace conflict: HDR requires linear, got ${colorSpace}`,
|
|
425
|
+
path: sourcePath
|
|
426
|
+
})
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const hdrRes = decodeHdr(bytes);
|
|
430
|
+
if (!hdrRes.ok) return err(hdrRes.error);
|
|
431
|
+
const rgba8 = new Uint8Array(hdrRes.value.width * hdrRes.value.height * 4);
|
|
432
|
+
for (let i = 0; i < hdrRes.value.data.length; i++) {
|
|
433
|
+
const v = hdrRes.value.data[i];
|
|
434
|
+
const clamped = v !== void 0 ? Math.max(0, Math.min(1, v)) : 0;
|
|
435
|
+
rgba8[i] = Math.round(clamped * 255);
|
|
436
|
+
}
|
|
437
|
+
const decoded = {
|
|
438
|
+
bytes: rgba8,
|
|
439
|
+
width: hdrRes.value.width,
|
|
440
|
+
height: hdrRes.value.height,
|
|
441
|
+
mime: "image/jpeg",
|
|
442
|
+
colorSpace: "linear",
|
|
443
|
+
mipmap: false
|
|
444
|
+
};
|
|
445
|
+
return ok({ decoded, meta });
|
|
446
|
+
}
|
|
447
|
+
const r = parseImage(bytes, mime, {
|
|
448
|
+
colorSpace,
|
|
449
|
+
mipmap: mipmap === "auto",
|
|
450
|
+
path: sourcePath
|
|
451
|
+
});
|
|
452
|
+
if (!r.ok) {
|
|
453
|
+
return err(r.error);
|
|
454
|
+
}
|
|
455
|
+
return ok({ decoded: r.value, meta });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export { decodeImageFromFile };
|
|
459
|
+
//# sourceMappingURL=decode-image-from-file.mjs.map
|
|
460
|
+
//# sourceMappingURL=decode-image-from-file.mjs.map
|