@forgeax/engine-image 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/decode-image-from-file.d.ts.map +1 -1
- package/dist/decode-image-from-file.mjs +5 -135
- package/dist/decode-image-from-file.mjs.map +1 -1
- package/dist/hdr-decoder.mjs +1 -1
- package/dist/hdr-decoder.mjs.map +1 -1
- package/dist/image-importer.d.ts.map +1 -1
- package/dist/image-importer.mjs +20 -100
- package/dist/image-importer.mjs.map +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +2 -36
- package/dist/index.mjs.map +1 -1
- package/dist/ktx2-encode.d.ts +2 -5
- package/dist/ktx2-encode.d.ts.map +1 -1
- package/dist/ktx2-encode.mjs +15 -17
- package/dist/ktx2-encode.mjs.map +1 -1
- package/dist/parse-image.d.ts +0 -1
- package/dist/parse-image.d.ts.map +1 -1
- package/dist/parse-image.mjs +3 -80
- package/dist/parse-image.mjs.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/image.unit.test.ts +28 -162
- package/src/decode-image-from-file.ts +3 -68
- package/src/errors.ts +1 -1
- package/src/image-decoder-browser.ts +1 -1
- package/src/image-importer.ts +3 -4
- package/src/index.ts +3 -5
- package/src/ktx2-encode.ts +17 -21
- package/src/parse-image.ts +3 -103
- package/dist/__tests__/ktx2-encode-mode-owner.test-d.d.ts +0 -2
- package/dist/__tests__/ktx2-encode-mode-owner.test-d.d.ts.map +0 -1
- package/dist/__tests__/runtime-decode.unit.test.d.ts +0 -2
- package/dist/__tests__/runtime-decode.unit.test.d.ts.map +0 -1
- package/dist/__tests__/tga.unit.test.d.ts +0 -2
- package/dist/__tests__/tga.unit.test.d.ts.map +0 -1
- package/dist/runtime/__tests__/asset-decoders.unit.test.d.ts +0 -2
- package/dist/runtime/__tests__/asset-decoders.unit.test.d.ts.map +0 -1
- package/dist/runtime/decode-image-bytes.d.ts +0 -7
- package/dist/runtime/decode-image-bytes.d.ts.map +0 -1
- package/dist/runtime/image-error.d.ts +0 -3
- package/dist/runtime/image-error.d.ts.map +0 -1
- package/src/__tests__/ktx2-encode-mode-owner.test-d.ts +0 -70
- package/src/__tests__/runtime-decode.unit.test.ts +0 -22
- package/src/__tests__/tga.unit.test.ts +0 -59
- package/src/runtime/__tests__/asset-decoders.unit.test.ts +0 -66
- package/src/runtime/decode-image-bytes.ts +0 -41
- package/src/runtime/image-error.ts +0 -8
package/dist/parse-image.mjs.map
CHANGED
|
@@ -1 +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', 'image/x-tga']; 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', 'image/x-tga'];\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 * - `image/x-tga` -> the built-in TGA true-color/grayscale decoder\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 if (mime === 'image/jpeg') {\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 } else {\n const decoded = decodeTga(bytes);\n width = decoded.width;\n height = decoded.height;\n rgba = decoded.bytes;\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' | 'image/x-tga',\n colorSpace: opts.colorSpace ?? 'srgb',\n mipmap: opts.mipmap ?? true,\n });\n}\n\ninterface TgaDecoded {\n readonly width: number;\n readonly height: number;\n readonly bytes: Uint8Array;\n}\n\n/**\n * Decode the portable TGA subset used by common FBX exports.\n *\n * The decoder owns the format boundary instead of trusting the extension: it\n * validates the header, supports uncompressed and RLE true-colour (24/32-bit)\n * plus grayscale (8-bit), and applies the origin bits from the image\n * descriptor while producing a top-left RGBA POD.\n */\nfunction decodeTga(bytes: Uint8Array): TgaDecoded {\n if (bytes.length < 18) throw new Error('TGA header is truncated');\n\n const colorMapType = bytes[1];\n const imageType = bytes[2];\n const idLength = bytes[0] ?? 0;\n const width = (bytes[12] ?? 0) | ((bytes[13] ?? 0) << 8);\n const height = (bytes[14] ?? 0) | ((bytes[15] ?? 0) << 8);\n const pixelDepth = bytes[16] ?? 0;\n const descriptor = bytes[17] ?? 0;\n if (colorMapType !== 0) throw new Error('color-mapped TGA is unsupported');\n if (width < 1 || height < 1) throw new Error('TGA dimensions must be positive');\n\n const isTrueColor = imageType === 2 || imageType === 10;\n const isGray = imageType === 3 || imageType === 11;\n if (!isTrueColor && !isGray) throw new Error(`TGA image type ${imageType} is unsupported`);\n if (isTrueColor && pixelDepth !== 24 && pixelDepth !== 32) {\n throw new Error(`TGA true-color depth ${pixelDepth} is unsupported`);\n }\n if (isGray && pixelDepth !== 8)\n throw new Error(`TGA grayscale depth ${pixelDepth} is unsupported`);\n\n const bytesPerPixel = pixelDepth / 8;\n let offset = 18 + idLength;\n const pixelCount = width * height;\n const out = new Uint8Array(pixelCount * 4);\n const topOrigin = (descriptor & 0x20) !== 0;\n const rightOrigin = (descriptor & 0x10) !== 0;\n let filePixel = 0;\n\n const writePixel = (pixel: Uint8Array): void => {\n if (filePixel >= pixelCount) throw new Error('TGA pixel data has too many pixels');\n const fileX = filePixel % width;\n const fileY = Math.floor(filePixel / width);\n const x = rightOrigin ? width - 1 - fileX : fileX;\n const y = topOrigin ? fileY : height - 1 - fileY;\n const destination = (y * width + x) * 4;\n if (isGray) {\n const value = pixel[0] ?? 0;\n out[destination] = value;\n out[destination + 1] = value;\n out[destination + 2] = value;\n out[destination + 3] = 255;\n } else {\n out[destination] = pixel[2] ?? 0;\n out[destination + 1] = pixel[1] ?? 0;\n out[destination + 2] = pixel[0] ?? 0;\n out[destination + 3] = bytesPerPixel === 4 ? (pixel[3] ?? 255) : 255;\n }\n filePixel += 1;\n };\n\n const readPixel = (): Uint8Array => {\n if (offset + bytesPerPixel > bytes.length) throw new Error('TGA pixel data is truncated');\n const pixel = bytes.slice(offset, offset + bytesPerPixel);\n offset += bytesPerPixel;\n return pixel;\n };\n\n if (imageType === 2 || imageType === 3) {\n while (filePixel < pixelCount) writePixel(readPixel());\n } else {\n while (filePixel < pixelCount) {\n if (offset >= bytes.length) throw new Error('TGA RLE packet header is truncated');\n const packet = bytes[offset++] ?? 0;\n const count = (packet & 0x7f) + 1;\n if (filePixel + count > pixelCount) throw new Error('TGA RLE packet exceeds image bounds');\n if ((packet & 0x80) !== 0) {\n const pixel = readPixel();\n for (let i = 0; i < count; i++) writePixel(pixel);\n } else {\n for (let i = 0; i < count; i++) writePixel(readPixel());\n }\n }\n }\n\n if (filePixel !== pixelCount) throw new Error('TGA pixel count mismatch');\n return { width, height, bytes: out };\n}\n"]}
|
|
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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-image",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -54,16 +54,16 @@
|
|
|
54
54
|
"LICENSE"
|
|
55
55
|
],
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@forgeax/engine-codec": "0.1.
|
|
58
|
-
"@forgeax/engine-math": "0.1.
|
|
59
|
-
"@forgeax/engine-pack": "0.1.
|
|
60
|
-
"@forgeax/engine-types": "0.1.
|
|
57
|
+
"@forgeax/engine-codec": "0.1.6",
|
|
58
|
+
"@forgeax/engine-math": "0.1.6",
|
|
59
|
+
"@forgeax/engine-pack": "0.1.6",
|
|
60
|
+
"@forgeax/engine-types": "0.1.6",
|
|
61
61
|
"jpeg-js": "^0.4.4",
|
|
62
62
|
"pako": "^1.0.11",
|
|
63
63
|
"upng-js": "^2.1.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@forgeax/engine-import": "0.1.
|
|
66
|
+
"@forgeax/engine-import": "0.1.6",
|
|
67
67
|
"@types/node": "^20.14.0",
|
|
68
68
|
"@webgpu/types": "^0.1.71"
|
|
69
69
|
},
|
|
@@ -50,32 +50,6 @@ import { subAssetKey, subAssetKeyEqual } from '../sub-asset-key.js';
|
|
|
50
50
|
import { toAssetPack } from '../to-asset-pack.js';
|
|
51
51
|
import { makeCorruptPng, makeJpg, makePng } from './make-fixture.js';
|
|
52
52
|
|
|
53
|
-
function makeMinimalHdr(): Uint8Array {
|
|
54
|
-
const header = '#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n\n-Y 1 +X 8\n';
|
|
55
|
-
const encoder = new TextEncoder();
|
|
56
|
-
const headerBytes = encoder.encode(header);
|
|
57
|
-
const width = 8;
|
|
58
|
-
const hi = (width >> 8) & 0xff;
|
|
59
|
-
const lo = width & 0xff;
|
|
60
|
-
const prefix = new Uint8Array([0x02, 0x02, hi, lo]);
|
|
61
|
-
const chRun = new Uint8Array([128 + 8, 128]);
|
|
62
|
-
const pixelBytes = new Uint8Array(4 + 4 * 2);
|
|
63
|
-
let off = 0;
|
|
64
|
-
pixelBytes.set(prefix, off);
|
|
65
|
-
off += 4;
|
|
66
|
-
pixelBytes.set(chRun, off);
|
|
67
|
-
off += 2;
|
|
68
|
-
pixelBytes.set(chRun, off);
|
|
69
|
-
off += 2;
|
|
70
|
-
pixelBytes.set(chRun, off);
|
|
71
|
-
off += 2;
|
|
72
|
-
pixelBytes.set(chRun, off);
|
|
73
|
-
const total = new Uint8Array(headerBytes.length + pixelBytes.length);
|
|
74
|
-
total.set(headerBytes);
|
|
75
|
-
total.set(pixelBytes, headerBytes.length);
|
|
76
|
-
return total;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
53
|
{
|
|
80
54
|
// ─── from decode-image-from-file.test.ts ───
|
|
81
55
|
|
|
@@ -210,89 +184,6 @@ function makeMinimalHdr(): Uint8Array {
|
|
|
210
184
|
}
|
|
211
185
|
});
|
|
212
186
|
|
|
213
|
-
it('rejects a parseable schema-invalid sidecar before decode and retries on the same path', async () => {
|
|
214
|
-
const dir = mkTmpDir('malformed-sidecar-retry');
|
|
215
|
-
const sourcePath = writeFixture(dir, 'staged.png', readBinDF('wood-1x1.png'));
|
|
216
|
-
const sidecarPath = `${sourcePath}.meta.json`;
|
|
217
|
-
const invalidSidecar = new TextEncoder().encode('{}');
|
|
218
|
-
const validSidecar = {
|
|
219
|
-
schemaVersion: '1.0.0',
|
|
220
|
-
kind: 'external-asset-package',
|
|
221
|
-
importer: 'image',
|
|
222
|
-
source: 'staged.png',
|
|
223
|
-
importSettings: {
|
|
224
|
-
colorSpace: 'linear',
|
|
225
|
-
mipmap: 'none',
|
|
226
|
-
addressMode: 'mirror-repeat',
|
|
227
|
-
filterMode: 'nearest',
|
|
228
|
-
},
|
|
229
|
-
subAssets: [
|
|
230
|
-
{
|
|
231
|
-
guid: '01928000-7c00-7000-8000-000000000095',
|
|
232
|
-
sourceIndex: 0,
|
|
233
|
-
kind: 'texture',
|
|
234
|
-
},
|
|
235
|
-
],
|
|
236
|
-
};
|
|
237
|
-
const validSidecarBytes = new TextEncoder().encode(JSON.stringify(validSidecar));
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
writeFileSync(sidecarPath, invalidSidecar);
|
|
241
|
-
const rejected = await decodeImageFromFile(sourcePath);
|
|
242
|
-
expect(rejected.ok).toBe(false);
|
|
243
|
-
if (rejected.ok) return;
|
|
244
|
-
expect(rejected.error.code).toBe('image-decode-failed');
|
|
245
|
-
if (rejected.error.detail.code !== 'image-decode-failed') return;
|
|
246
|
-
expect(rejected.error.detail.path).toBe(sidecarPath);
|
|
247
|
-
expect(rejected.error.detail.reason).toMatch(/^sidecar schema validation failed:/);
|
|
248
|
-
|
|
249
|
-
writeFileSync(sidecarPath, validSidecarBytes);
|
|
250
|
-
const recovered = await decodeImageFromFile(sourcePath);
|
|
251
|
-
expect(recovered.ok).toBe(true);
|
|
252
|
-
if (!recovered.ok) return;
|
|
253
|
-
expect(recovered.value.decoded).toMatchObject({
|
|
254
|
-
width: 1,
|
|
255
|
-
height: 1,
|
|
256
|
-
mime: 'image/png',
|
|
257
|
-
colorSpace: 'linear',
|
|
258
|
-
mipmap: false,
|
|
259
|
-
});
|
|
260
|
-
expect(recovered.value.meta).toEqual({
|
|
261
|
-
guid: '01928000-7c00-7000-8000-000000000095',
|
|
262
|
-
colorSpace: 'linear',
|
|
263
|
-
mipmap: 'none',
|
|
264
|
-
addressMode: 'mirror-repeat',
|
|
265
|
-
filterMode: 'nearest',
|
|
266
|
-
});
|
|
267
|
-
expect(toAssetPack(recovered.value.decoded, recovered.value.meta)).toEqual({
|
|
268
|
-
schemaVersion: '1.0.0',
|
|
269
|
-
kind: 'external-asset-package',
|
|
270
|
-
importer: 'image',
|
|
271
|
-
source: '',
|
|
272
|
-
importSettings: {
|
|
273
|
-
colorSpace: 'linear',
|
|
274
|
-
mipmap: 'none',
|
|
275
|
-
addressMode: 'mirror-repeat',
|
|
276
|
-
filterMode: 'nearest',
|
|
277
|
-
},
|
|
278
|
-
subAssets: [
|
|
279
|
-
{
|
|
280
|
-
guid: '01928000-7c00-7000-8000-000000000095',
|
|
281
|
-
sourceIndex: 0,
|
|
282
|
-
kind: 'texture',
|
|
283
|
-
},
|
|
284
|
-
],
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
const repeated = await decodeImageFromFile(sourcePath);
|
|
288
|
-
expect(repeated).toEqual(recovered);
|
|
289
|
-
expect(readFileSync(sourcePath)).toEqual(Buffer.from(readBinDF('wood-1x1.png')));
|
|
290
|
-
expect(readFileSync(sidecarPath)).toEqual(Buffer.from(validSidecarBytes));
|
|
291
|
-
} finally {
|
|
292
|
-
rmSync(dir, { recursive: true, force: true });
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
|
|
296
187
|
it('decodes a valid 1x1 PNG file when sidecar is present, returns DecodedImage + ImageMeta', async () => {
|
|
297
188
|
const dir = mkTmpDir('happy-png');
|
|
298
189
|
try {
|
|
@@ -370,51 +261,6 @@ function makeMinimalHdr(): Uint8Array {
|
|
|
370
261
|
}
|
|
371
262
|
});
|
|
372
263
|
|
|
373
|
-
it('keeps HDR equirect sidecars accepted by the file decoder', async () => {
|
|
374
|
-
const dir = mkTmpDir('happy-hdr');
|
|
375
|
-
const sourcePath = writeFixture(dir, 'environment.hdr', makeMinimalHdr());
|
|
376
|
-
const sidecar = {
|
|
377
|
-
schemaVersion: '1.0.0',
|
|
378
|
-
kind: 'external-asset-package',
|
|
379
|
-
importer: 'image',
|
|
380
|
-
source: 'environment.hdr',
|
|
381
|
-
importSettings: {
|
|
382
|
-
colorSpace: 'linear',
|
|
383
|
-
mipmap: 'none',
|
|
384
|
-
addressMode: 'repeat',
|
|
385
|
-
filterMode: 'linear',
|
|
386
|
-
},
|
|
387
|
-
subAssets: [
|
|
388
|
-
{
|
|
389
|
-
guid: '019e3969-1d43-7610-8810-e80dbd491d91',
|
|
390
|
-
sourceIndex: 0,
|
|
391
|
-
kind: 'equirect',
|
|
392
|
-
},
|
|
393
|
-
],
|
|
394
|
-
};
|
|
395
|
-
try {
|
|
396
|
-
writeFixture(
|
|
397
|
-
dir,
|
|
398
|
-
'environment.hdr.meta.json',
|
|
399
|
-
new TextEncoder().encode(JSON.stringify(sidecar)),
|
|
400
|
-
);
|
|
401
|
-
const r = await decodeImageFromFile(sourcePath);
|
|
402
|
-
expect(r.ok).toBe(true);
|
|
403
|
-
if (!r.ok) return;
|
|
404
|
-
expect(r.value.decoded.width).toBe(8);
|
|
405
|
-
expect(r.value.decoded.height).toBe(1);
|
|
406
|
-
expect(r.value.meta).toEqual({
|
|
407
|
-
guid: '019e3969-1d43-7610-8810-e80dbd491d91',
|
|
408
|
-
colorSpace: 'linear',
|
|
409
|
-
mipmap: 'none',
|
|
410
|
-
addressMode: 'repeat',
|
|
411
|
-
filterMode: 'linear',
|
|
412
|
-
});
|
|
413
|
-
} finally {
|
|
414
|
-
rmSync(dir, { recursive: true, force: true });
|
|
415
|
-
}
|
|
416
|
-
});
|
|
417
|
-
|
|
418
264
|
it('returns image-format-unsupported when extension is not .jpg/.jpeg/.png', async () => {
|
|
419
265
|
const dir = mkTmpDir('unsupported-ext');
|
|
420
266
|
try {
|
|
@@ -455,6 +301,32 @@ function makeMinimalHdr(): Uint8Array {
|
|
|
455
301
|
{
|
|
456
302
|
// ─── from hdr-decoder.test.ts ───
|
|
457
303
|
|
|
304
|
+
function makeMinimalHdr(): Uint8Array {
|
|
305
|
+
const header = '#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n\n-Y 1 +X 8\n';
|
|
306
|
+
const encoder = new TextEncoder();
|
|
307
|
+
const headerBytes = encoder.encode(header);
|
|
308
|
+
const width = 8;
|
|
309
|
+
const hi = (width >> 8) & 0xff;
|
|
310
|
+
const lo = width & 0xff;
|
|
311
|
+
const prefix = new Uint8Array([0x02, 0x02, hi, lo]);
|
|
312
|
+
const chRun = new Uint8Array([128 + 8, 128]);
|
|
313
|
+
const pixelBytes = new Uint8Array(4 + 4 * 2);
|
|
314
|
+
let off = 0;
|
|
315
|
+
pixelBytes.set(prefix, off);
|
|
316
|
+
off += 4;
|
|
317
|
+
pixelBytes.set(chRun, off);
|
|
318
|
+
off += 2;
|
|
319
|
+
pixelBytes.set(chRun, off);
|
|
320
|
+
off += 2;
|
|
321
|
+
pixelBytes.set(chRun, off);
|
|
322
|
+
off += 2;
|
|
323
|
+
pixelBytes.set(chRun, off);
|
|
324
|
+
const total = new Uint8Array(headerBytes.length + pixelBytes.length);
|
|
325
|
+
total.set(headerBytes);
|
|
326
|
+
total.set(pixelBytes, headerBytes.length);
|
|
327
|
+
return total;
|
|
328
|
+
}
|
|
329
|
+
|
|
458
330
|
function makeBlackHdr(): Uint8Array {
|
|
459
331
|
const header = '#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n\n-Y 1 +X 8\n';
|
|
460
332
|
const encoder = new TextEncoder();
|
|
@@ -897,17 +769,11 @@ function makeMinimalHdr(): Uint8Array {
|
|
|
897
769
|
describe('imageImporter unknown extension fail-fast', () => {
|
|
898
770
|
const dummyBytes = new Uint8Array([0x00, 0x00, 0x00, 0x00]);
|
|
899
771
|
|
|
900
|
-
it('.tga source
|
|
772
|
+
it('.tga source still throws unsupported source extension', async () => {
|
|
901
773
|
const ctx = makeHdrCtx('texture.tga', dummyBytes, [
|
|
902
774
|
{ guid: HDR_GUID, sourceIndex: 0, kind: 'texture' },
|
|
903
775
|
]);
|
|
904
|
-
|
|
905
|
-
expect(result.ok).toBe(false);
|
|
906
|
-
if (result.ok) return;
|
|
907
|
-
expect(result.error.code).toBe('source-validation-failed');
|
|
908
|
-
expect(result.error.detail).toMatchObject({
|
|
909
|
-
diagnostics: [expect.objectContaining({ code: 'image-conversion-decode-ldr' })],
|
|
910
|
-
});
|
|
776
|
+
await expect(imageImporter.import(ctx)).rejects.toThrow('unsupported source extension');
|
|
911
777
|
});
|
|
912
778
|
|
|
913
779
|
it('.bmp source still throws unsupported source extension', async () => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { readFile, stat } from 'node:fs/promises';
|
|
2
2
|
import { dirname, extname, join } from 'node:path';
|
|
3
3
|
|
|
4
|
-
import { validateMeta } from '@forgeax/engine-pack';
|
|
5
4
|
import type { DecodedImage, ImageError, ImageMeta } from '@forgeax/engine-types';
|
|
6
5
|
import { imageError } from './errors.js';
|
|
7
6
|
import { decodeHdr, type HdrDecoded } from './hdr-decoder.js';
|
|
@@ -52,13 +51,10 @@ export interface DecodedHdrWithMeta {
|
|
|
52
51
|
readonly meta: ImageMeta;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
const EXT_TO_MIME: Readonly<
|
|
56
|
-
Record<string, 'image/png' | 'image/jpeg' | 'image/x-tga' | 'image/vnd.radiance'>
|
|
57
|
-
> = {
|
|
54
|
+
const EXT_TO_MIME: Readonly<Record<string, 'image/png' | 'image/jpeg' | 'image/vnd.radiance'>> = {
|
|
58
55
|
'.png': 'image/png',
|
|
59
56
|
'.jpg': 'image/jpeg',
|
|
60
57
|
'.jpeg': 'image/jpeg',
|
|
61
|
-
'.tga': 'image/x-tga',
|
|
62
58
|
'.hdr': 'image/vnd.radiance',
|
|
63
59
|
};
|
|
64
60
|
|
|
@@ -71,45 +67,6 @@ function deriveSidecarPath(sourcePath: string): string {
|
|
|
71
67
|
return join(dir, `${base}.meta.json`);
|
|
72
68
|
}
|
|
73
69
|
|
|
74
|
-
function sidecarSchemaFailureReason(): string {
|
|
75
|
-
const firstError = validateMeta.errors?.[0];
|
|
76
|
-
if (firstError === undefined) return 'sidecar schema validation failed';
|
|
77
|
-
const location = firstError.instancePath === '' ? '/' : firstError.instancePath;
|
|
78
|
-
return `sidecar schema validation failed: ${firstError.keyword} at ${location}`.slice(0, 256);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function imageSidecarFailureReason(
|
|
82
|
-
sidecar: SidecarMeta,
|
|
83
|
-
mime: (typeof EXT_TO_MIME)[keyof typeof EXT_TO_MIME],
|
|
84
|
-
): string | undefined {
|
|
85
|
-
const subAsset = sidecar.subAssets[0];
|
|
86
|
-
const settings = sidecar.importSettings;
|
|
87
|
-
const expectedKind = mime === 'image/vnd.radiance' ? 'equirect' : 'texture';
|
|
88
|
-
if (sidecar.importer !== 'image') return 'image sidecar importer must be image';
|
|
89
|
-
if (sidecar.subAssets.length !== 1 || subAsset === undefined) {
|
|
90
|
-
return 'image sidecar must declare one sub-asset';
|
|
91
|
-
}
|
|
92
|
-
if (subAsset.kind !== expectedKind || subAsset.sourceIndex !== 0) {
|
|
93
|
-
return `image sidecar sub-asset must be ${expectedKind} at sourceIndex 0`;
|
|
94
|
-
}
|
|
95
|
-
if (settings.colorSpace !== undefined && !['srgb', 'linear'].includes(settings.colorSpace)) {
|
|
96
|
-
return 'image sidecar colorSpace must be srgb or linear';
|
|
97
|
-
}
|
|
98
|
-
if (settings.mipmap !== undefined && !['auto', 'none'].includes(settings.mipmap)) {
|
|
99
|
-
return 'image sidecar mipmap must be auto or none';
|
|
100
|
-
}
|
|
101
|
-
if (
|
|
102
|
-
settings.addressMode !== undefined &&
|
|
103
|
-
!['repeat', 'clamp-to-edge', 'mirror-repeat'].includes(settings.addressMode)
|
|
104
|
-
) {
|
|
105
|
-
return 'image sidecar addressMode is unsupported';
|
|
106
|
-
}
|
|
107
|
-
if (settings.filterMode !== undefined && !['nearest', 'linear'].includes(settings.filterMode)) {
|
|
108
|
-
return 'image sidecar filterMode is unsupported';
|
|
109
|
-
}
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
70
|
/**
|
|
114
71
|
* Async file-system entry to the image importer (plan-strategy section 3.2
|
|
115
72
|
* sequence A; AC-17 path (a) sidecar three-way fallback).
|
|
@@ -182,9 +139,9 @@ export async function decodeImageFromFile(
|
|
|
182
139
|
);
|
|
183
140
|
}
|
|
184
141
|
|
|
185
|
-
let
|
|
142
|
+
let sidecar: SidecarMeta;
|
|
186
143
|
try {
|
|
187
|
-
|
|
144
|
+
sidecar = JSON.parse(sidecarText) as SidecarMeta;
|
|
188
145
|
} catch (e) {
|
|
189
146
|
return err(
|
|
190
147
|
imageError({
|
|
@@ -195,28 +152,6 @@ export async function decodeImageFromFile(
|
|
|
195
152
|
);
|
|
196
153
|
}
|
|
197
154
|
|
|
198
|
-
if (!validateMeta(parsedSidecar)) {
|
|
199
|
-
return err(
|
|
200
|
-
imageError({
|
|
201
|
-
code: 'image-decode-failed',
|
|
202
|
-
reason: sidecarSchemaFailureReason(),
|
|
203
|
-
path: sidecarPath,
|
|
204
|
-
}),
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const sidecar = parsedSidecar as unknown as SidecarMeta;
|
|
209
|
-
const imageSidecarFailure = imageSidecarFailureReason(sidecar, mime);
|
|
210
|
-
if (imageSidecarFailure !== undefined) {
|
|
211
|
-
return err(
|
|
212
|
-
imageError({
|
|
213
|
-
code: 'image-decode-failed',
|
|
214
|
-
reason: imageSidecarFailure,
|
|
215
|
-
path: sidecarPath,
|
|
216
|
-
}),
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
155
|
const settings = sidecar.importSettings ?? {};
|
|
221
156
|
const colorSpace: ImageMeta['colorSpace'] = settings.colorSpace ?? 'srgb';
|
|
222
157
|
const mipmap: ImageMeta['mipmap'] = settings.mipmap ?? 'auto';
|
package/src/errors.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { IMAGE_ERROR_HINTS } from '@forgeax/engine-types';
|
|
|
8
8
|
const IMAGE_ERROR_EXPECTED: Readonly<Record<ImageErrorCode, string>> = {
|
|
9
9
|
'image-decode-failed': 'PNG / JPG byte stream decodes successfully',
|
|
10
10
|
'image-format-unsupported':
|
|
11
|
-
"mime is one of ['image/png', 'image/jpeg'
|
|
11
|
+
"mime is one of ['image/png', 'image/jpeg']; uploadTexture format <-> colorSpace family agrees",
|
|
12
12
|
'image-dimension-out-of-bounds':
|
|
13
13
|
'width and height fall under device caps maxTextureDimension2D (or 16384 hard cap)',
|
|
14
14
|
'image-meta-missing':
|
|
@@ -51,7 +51,7 @@ export async function decodeImageInBrowser(
|
|
|
51
51
|
// Read pixels back via OffscreenCanvas (browser context) -- 2D context is
|
|
52
52
|
// ubiquitous and avoids the WebGPU readback round-trip during disk-side
|
|
53
53
|
// decode (charter P5 producer / consumer split: GPU upload happens inside
|
|
54
|
-
//
|
|
54
|
+
// @forgeax/engine-runtime AssetRegistry.uploadTexture, not here).
|
|
55
55
|
const canvas =
|
|
56
56
|
typeof OffscreenCanvas !== 'undefined'
|
|
57
57
|
? new OffscreenCanvas(bitmap.width, bitmap.height)
|
package/src/image-importer.ts
CHANGED
|
@@ -48,11 +48,10 @@ import { encodeTextureToKtx2, resolveEncodeMode } from './ktx2-encode.js';
|
|
|
48
48
|
import { parseImage } from './parse-image.js';
|
|
49
49
|
|
|
50
50
|
/** Map a source path / mime hint to the parseImage mime literal. */
|
|
51
|
-
function mimeFromSource(source: string): 'image/png' | 'image/jpeg' |
|
|
51
|
+
function mimeFromSource(source: string): 'image/png' | 'image/jpeg' | undefined {
|
|
52
52
|
const lower = source.toLowerCase();
|
|
53
53
|
if (lower.endsWith('.png')) return 'image/png';
|
|
54
54
|
if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';
|
|
55
|
-
if (lower.endsWith('.tga')) return 'image/x-tga';
|
|
56
55
|
return undefined;
|
|
57
56
|
}
|
|
58
57
|
|
|
@@ -676,7 +675,7 @@ async function importImage(ctx: ImportContext): Promise<ImportResult> {
|
|
|
676
675
|
// --- Standard PNG/JPEG path ---
|
|
677
676
|
if (mime === undefined) {
|
|
678
677
|
throw new Error(
|
|
679
|
-
`imageImporter: unsupported source extension for "${ctx.source}" (expected .png / .jpg / .jpeg / .
|
|
678
|
+
`imageImporter: unsupported source extension for "${ctx.source}" (expected .png / .jpg / .jpeg / .hdr / .ktx2 / .basis)`,
|
|
680
679
|
);
|
|
681
680
|
}
|
|
682
681
|
|
|
@@ -703,7 +702,7 @@ async function importImage(ctx: ImportContext): Promise<ImportResult> {
|
|
|
703
702
|
'decode',
|
|
704
703
|
'ldr',
|
|
705
704
|
ctx.source,
|
|
706
|
-
'valid PNG
|
|
705
|
+
'valid PNG or JPEG bytes to decode into RGBA pixels',
|
|
707
706
|
`image:${decoded.error.code}`,
|
|
708
707
|
'repair the source image bytes and retry the import',
|
|
709
708
|
),
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @forgeax/engine-image -- main entry public surface.
|
|
2
2
|
//
|
|
3
3
|
// Browser-safe pure functions translate disk-side image bytes + sidecar
|
|
4
|
-
// settings into POD that the runtime
|
|
5
|
-
// (plan-strategy section 3.2 sequence A; charter P5 producer /
|
|
4
|
+
// settings into POD that the runtime AssetRegistry.uploadTexture path
|
|
5
|
+
// consumes (plan-strategy section 3.2 sequence A; charter P5 producer /
|
|
6
6
|
// consumer split). Node-only decoders (parseImage / decodeImageFromFile)
|
|
7
7
|
// are split into Node-only sub-exports per feat-20260524-browser-safe-subexports
|
|
8
8
|
// and are NOT re-exported from this main entry -- import them via:
|
|
@@ -31,9 +31,7 @@ export type {
|
|
|
31
31
|
export { reimportReuseMeta } from './reimport-reuse-meta.js';
|
|
32
32
|
export type { Result, ResultErr, ResultOk } from './result.js';
|
|
33
33
|
export { err, ok } from './result.js';
|
|
34
|
-
export { equirectContribution, textureContribution } from './runtime/asset-decoders
|
|
35
|
-
export { decodeImageBytes } from './runtime/decode-image-bytes.js';
|
|
36
|
-
export { runtimeImageError } from './runtime/image-error.js';
|
|
34
|
+
export { equirectContribution, textureContribution } from './runtime/asset-decoders';
|
|
37
35
|
export type { ImageSourceKeyLocator } from './source-key.js';
|
|
38
36
|
export { deriveImageSourceKey } from './source-key.js';
|
|
39
37
|
export type { SubAssetKey, SubAssetKeyInput } from './sub-asset-key.js';
|
package/src/ktx2-encode.ts
CHANGED
|
@@ -20,15 +20,11 @@
|
|
|
20
20
|
import type { BasisEncodeMode } from '@forgeax/engine-codec/encode';
|
|
21
21
|
import { basisEncode } from '@forgeax/engine-codec/encode';
|
|
22
22
|
|
|
23
|
-
const COMPRESSION_MODES = ['auto', 'etc1s', 'uastc', 'none'] as const;
|
|
24
|
-
|
|
25
23
|
/** The sidecar control-plane value (ImageMetadata.compressionMode). */
|
|
26
|
-
export type CompressionMode =
|
|
27
|
-
|
|
28
|
-
const RESOLVED_ENCODE_MODES = ['etc1s', 'uastc', 'uastc-hdr', 'none'] as const;
|
|
24
|
+
export type CompressionMode = 'auto' | 'etc1s' | 'uastc' | 'none';
|
|
29
25
|
|
|
30
26
|
/** The resolved delivery encoding after 'auto' derivation. `'none'` = no encode. */
|
|
31
|
-
export type ResolvedEncodeMode =
|
|
27
|
+
export type ResolvedEncodeMode = 'etc1s' | 'uastc' | 'uastc-hdr' | 'none';
|
|
32
28
|
|
|
33
29
|
/** Source signals that drive 'auto' derivation (D-12) and encoder params. */
|
|
34
30
|
export interface EncodeSourceInfo {
|
|
@@ -56,15 +52,15 @@ export function resolveEncodeMode(
|
|
|
56
52
|
source: EncodeSourceInfo,
|
|
57
53
|
): ResolvedEncodeMode {
|
|
58
54
|
switch (mode) {
|
|
59
|
-
case
|
|
60
|
-
return
|
|
61
|
-
case
|
|
62
|
-
return
|
|
63
|
-
case
|
|
64
|
-
return source.isHdr ?
|
|
65
|
-
case
|
|
66
|
-
if (source.isHdr) return
|
|
67
|
-
return source.colorSpace === 'srgb' ?
|
|
55
|
+
case 'none':
|
|
56
|
+
return 'none';
|
|
57
|
+
case 'etc1s':
|
|
58
|
+
return 'etc1s';
|
|
59
|
+
case 'uastc':
|
|
60
|
+
return source.isHdr ? 'uastc-hdr' : 'uastc';
|
|
61
|
+
case 'auto':
|
|
62
|
+
if (source.isHdr) return 'uastc-hdr';
|
|
63
|
+
return source.colorSpace === 'srgb' ? 'etc1s' : 'uastc';
|
|
68
64
|
}
|
|
69
65
|
}
|
|
70
66
|
|
|
@@ -94,17 +90,17 @@ export function basisEncodeParamsFor(
|
|
|
94
90
|
const resolved = resolveEncodeMode(mode, source);
|
|
95
91
|
const srgbColor = source.colorSpace === 'srgb';
|
|
96
92
|
switch (resolved) {
|
|
97
|
-
case
|
|
93
|
+
case 'none':
|
|
98
94
|
return null;
|
|
99
|
-
case
|
|
95
|
+
case 'etc1s':
|
|
100
96
|
return {
|
|
101
|
-
mode:
|
|
97
|
+
mode: 'etc1s',
|
|
102
98
|
srgb: srgbColor,
|
|
103
99
|
perceptual: srgbColor,
|
|
104
100
|
uastcSupercompression: false,
|
|
105
101
|
mipGen: false,
|
|
106
102
|
};
|
|
107
|
-
case
|
|
103
|
+
case 'uastc':
|
|
108
104
|
return {
|
|
109
105
|
mode: 'uastc-ldr',
|
|
110
106
|
srgb: srgbColor,
|
|
@@ -112,9 +108,9 @@ export function basisEncodeParamsFor(
|
|
|
112
108
|
uastcSupercompression: true,
|
|
113
109
|
mipGen: false,
|
|
114
110
|
};
|
|
115
|
-
case
|
|
111
|
+
case 'uastc-hdr':
|
|
116
112
|
return {
|
|
117
|
-
mode:
|
|
113
|
+
mode: 'uastc-hdr',
|
|
118
114
|
srgb: false,
|
|
119
115
|
perceptual: false,
|
|
120
116
|
uastcSupercompression: false,
|