@forgeax/engine-image 0.1.4 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +5 -7
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/decode-image-from-file.d.ts.map +1 -1
  4. package/dist/decode-image-from-file.mjs +5 -135
  5. package/dist/decode-image-from-file.mjs.map +1 -1
  6. package/dist/hdr-decoder.mjs +1 -1
  7. package/dist/hdr-decoder.mjs.map +1 -1
  8. package/dist/image-importer.d.ts.map +1 -1
  9. package/dist/image-importer.mjs +20 -100
  10. package/dist/image-importer.mjs.map +1 -1
  11. package/dist/index.d.ts +1 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.mjs +2 -36
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/ktx2-encode.d.ts +2 -5
  16. package/dist/ktx2-encode.d.ts.map +1 -1
  17. package/dist/ktx2-encode.mjs +15 -17
  18. package/dist/ktx2-encode.mjs.map +1 -1
  19. package/dist/parse-image.d.ts +0 -1
  20. package/dist/parse-image.d.ts.map +1 -1
  21. package/dist/parse-image.mjs +3 -80
  22. package/dist/parse-image.mjs.map +1 -1
  23. package/package.json +6 -6
  24. package/src/__tests__/image.unit.test.ts +28 -162
  25. package/src/decode-image-from-file.ts +3 -68
  26. package/src/errors.ts +1 -1
  27. package/src/image-decoder-browser.ts +1 -1
  28. package/src/image-importer.ts +3 -4
  29. package/src/index.ts +3 -5
  30. package/src/ktx2-encode.ts +17 -21
  31. package/src/parse-image.ts +3 -103
  32. package/dist/__tests__/ktx2-encode-mode-owner.test-d.d.ts +0 -2
  33. package/dist/__tests__/ktx2-encode-mode-owner.test-d.d.ts.map +0 -1
  34. package/dist/__tests__/runtime-decode.unit.test.d.ts +0 -2
  35. package/dist/__tests__/runtime-decode.unit.test.d.ts.map +0 -1
  36. package/dist/__tests__/tga.unit.test.d.ts +0 -2
  37. package/dist/__tests__/tga.unit.test.d.ts.map +0 -1
  38. package/dist/runtime/__tests__/asset-decoders.unit.test.d.ts +0 -2
  39. package/dist/runtime/__tests__/asset-decoders.unit.test.d.ts.map +0 -1
  40. package/dist/runtime/decode-image-bytes.d.ts +0 -7
  41. package/dist/runtime/decode-image-bytes.d.ts.map +0 -1
  42. package/dist/runtime/image-error.d.ts +0 -3
  43. package/dist/runtime/image-error.d.ts.map +0 -1
  44. package/src/__tests__/ktx2-encode-mode-owner.test-d.ts +0 -70
  45. package/src/__tests__/runtime-decode.unit.test.ts +0 -22
  46. package/src/__tests__/tga.unit.test.ts +0 -59
  47. package/src/runtime/__tests__/asset-decoders.unit.test.ts +0 -66
  48. package/src/runtime/decode-image-bytes.ts +0 -41
  49. package/src/runtime/image-error.ts +0 -8
@@ -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.4",
3
+ "version": "0.1.7",
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.4",
58
- "@forgeax/engine-math": "0.1.4",
59
- "@forgeax/engine-pack": "0.1.4",
60
- "@forgeax/engine-types": "0.1.4",
57
+ "@forgeax/engine-codec": "0.1.7",
58
+ "@forgeax/engine-math": "0.1.7",
59
+ "@forgeax/engine-pack": "0.1.7",
60
+ "@forgeax/engine-types": "0.1.7",
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.4",
66
+ "@forgeax/engine-import": "0.1.7",
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 is recognized and invalid bytes fail during decode', async () => {
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
- const result = await imageImporter.import(ctx);
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 parsedSidecar: unknown;
142
+ let sidecar: SidecarMeta;
186
143
  try {
187
- parsedSidecar = JSON.parse(sidecarText) as unknown;
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', 'image/x-tga']; uploadTexture format <-> colorSpace family agrees",
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
- // the Render DeviceScope, not in this byte decoder).
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)
@@ -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' | 'image/x-tga' | undefined {
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 / .tga / .hdr / .ktx2 / .basis)`,
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, JPEG, or TGA bytes to decode into RGBA pixels',
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 Render DeviceScope path consumes
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.js';
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';
@@ -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 = (typeof COMPRESSION_MODES)[number];
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 = (typeof RESOLVED_ENCODE_MODES)[number];
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 COMPRESSION_MODES[3]:
60
- return RESOLVED_ENCODE_MODES[3];
61
- case COMPRESSION_MODES[1]:
62
- return RESOLVED_ENCODE_MODES[0];
63
- case COMPRESSION_MODES[2]:
64
- return source.isHdr ? RESOLVED_ENCODE_MODES[2] : RESOLVED_ENCODE_MODES[1];
65
- case COMPRESSION_MODES[0]:
66
- if (source.isHdr) return RESOLVED_ENCODE_MODES[2];
67
- return source.colorSpace === 'srgb' ? RESOLVED_ENCODE_MODES[0] : RESOLVED_ENCODE_MODES[1];
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 RESOLVED_ENCODE_MODES[3]:
93
+ case 'none':
98
94
  return null;
99
- case RESOLVED_ENCODE_MODES[0]:
95
+ case 'etc1s':
100
96
  return {
101
- mode: RESOLVED_ENCODE_MODES[0],
97
+ mode: 'etc1s',
102
98
  srgb: srgbColor,
103
99
  perceptual: srgbColor,
104
100
  uastcSupercompression: false,
105
101
  mipGen: false,
106
102
  };
107
- case RESOLVED_ENCODE_MODES[1]:
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 RESOLVED_ENCODE_MODES[2]:
111
+ case 'uastc-hdr':
116
112
  return {
117
- mode: RESOLVED_ENCODE_MODES[2],
113
+ mode: 'uastc-hdr',
118
114
  srgb: false,
119
115
  perceptual: false,
120
116
  uastcSupercompression: false,