@forgeax/engine-gltf 0.0.0-dev.8d955ade1c79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +204 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/animation-target-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-target-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-material-samplers.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-material-samplers.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-material-values.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-material-values.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-mixed-color.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-mixed-color.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-sampler-required.test-d.d.ts +2 -0
- package/dist/__tests__/bridge-sampler-required.test-d.d.ts.map +1 -0
- package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-skin-stride.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-skin-stride.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-test-helpers.d.ts +5 -0
- package/dist/__tests__/bridge-test-helpers.d.ts.map +1 -0
- package/dist/__tests__/bridge.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge.unit.test.d.ts.map +1 -0
- package/dist/__tests__/cli-gltf.integration.test.d.ts +2 -0
- package/dist/__tests__/cli-gltf.integration.test.d.ts.map +1 -0
- package/dist/__tests__/color-0-accessor.unit.test.d.ts +2 -0
- package/dist/__tests__/color-0-accessor.unit.test.d.ts.map +1 -0
- package/dist/__tests__/data-uri-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/data-uri-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/extension-admission.unit.test.d.ts +2 -0
- package/dist/__tests__/extension-admission.unit.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-derived-views.test-d.d.ts +2 -0
- package/dist/__tests__/gltf-error-derived-views.test-d.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-migration.test.d.ts +2 -0
- package/dist/__tests__/gltf-error-migration.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts +2 -0
- package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-importer.test.d.ts +2 -0
- package/dist/__tests__/gltf-importer.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-local-artifacts.test.d.ts +2 -0
- package/dist/__tests__/gltf-local-artifacts.test.d.ts.map +1 -0
- package/dist/__tests__/gltf.unit.test.d.ts +2 -0
- package/dist/__tests__/gltf.unit.test.d.ts.map +1 -0
- package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts +2 -0
- package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-missing-uv.unit.test.d.ts +2 -0
- package/dist/__tests__/material-missing-uv.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-pack-refs.integration.test.d.ts +2 -0
- package/dist/__tests__/material-pack-refs.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-per-slot-ir.unit.test.d.ts +2 -0
- package/dist/__tests__/material-per-slot-ir.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-texture-transform.unit.test.d.ts +2 -0
- package/dist/__tests__/material-texture-transform.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-unused-uv.unit.test.d.ts +2 -0
- package/dist/__tests__/material-unused-uv.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-decode.unit.test.d.ts +2 -0
- package/dist/__tests__/meshopt-decode.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-falsifier.unit.test.d.ts +2 -0
- package/dist/__tests__/meshopt-falsifier.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts +2 -0
- package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts +2 -0
- package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts.map +1 -0
- package/dist/__tests__/morph-import.integration.test.d.ts +2 -0
- package/dist/__tests__/morph-import.integration.test.d.ts.map +1 -0
- package/dist/__tests__/morph-import.unit.test.d.ts +2 -0
- package/dist/__tests__/morph-import.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-decode.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-decode.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf-multi-uv.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf-multi-uv.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pick-e2e.integration.test.d.ts +2 -0
- package/dist/__tests__/pick-e2e.integration.test.d.ts.map +1 -0
- package/dist/__tests__/public-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/public-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/source-key-diagnostics.integration.test.d.ts +2 -0
- package/dist/__tests__/source-key-diagnostics.integration.test.d.ts.map +1 -0
- package/dist/__tests__/source-key-producer.integration.test.d.ts +2 -0
- package/dist/__tests__/source-key-producer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/source-key.unit.test.d.ts +2 -0
- package/dist/__tests__/source-key.unit.test.d.ts.map +1 -0
- package/dist/accessor/decode-accessor.d.ts +75 -0
- package/dist/accessor/decode-accessor.d.ts.map +1 -0
- package/dist/accessor/decode-color.d.ts +15 -0
- package/dist/accessor/decode-color.d.ts.map +1 -0
- package/dist/accessor/index.d.ts +3 -0
- package/dist/accessor/index.d.ts.map +1 -0
- package/dist/bridge.d.ts +103 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/check-extensions.d.ts +29 -0
- package/dist/check-extensions.d.ts.map +1 -0
- package/dist/cli-gltf.d.ts +8 -0
- package/dist/cli-gltf.d.ts.map +1 -0
- package/dist/cli-gltf.mjs +2435 -0
- package/dist/cli-gltf.mjs.map +1 -0
- package/dist/data-uri.d.ts +6 -0
- package/dist/data-uri.d.ts.map +1 -0
- package/dist/errors.d.ts +201 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/extract-image-bytes.d.ts +42 -0
- package/dist/extract-image-bytes.d.ts.map +1 -0
- package/dist/gltf-importer.d.ts +21 -0
- package/dist/gltf-importer.d.ts.map +1 -0
- package/dist/image-color-space.d.ts +35 -0
- package/dist/image-color-space.d.ts.map +1 -0
- package/dist/importer-entry.d.ts +10 -0
- package/dist/importer-entry.d.ts.map +1 -0
- package/dist/importer-entry.mjs +3378 -0
- package/dist/importer-entry.mjs.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +3673 -0
- package/dist/index.mjs.map +1 -0
- package/dist/meshopt-decode.d.ts +39 -0
- package/dist/meshopt-decode.d.ts.map +1 -0
- package/dist/node-file-entry.d.ts +14 -0
- package/dist/node-file-entry.d.ts.map +1 -0
- package/dist/node-file-entry.mjs +1946 -0
- package/dist/node-file-entry.mjs.map +1 -0
- package/dist/node-path.d.ts +14 -0
- package/dist/node-path.d.ts.map +1 -0
- package/dist/parse-animation.d.ts +70 -0
- package/dist/parse-animation.d.ts.map +1 -0
- package/dist/parse-glb-chunks.d.ts +25 -0
- package/dist/parse-glb-chunks.d.ts.map +1 -0
- package/dist/parse-glb.d.ts +3 -0
- package/dist/parse-glb.d.ts.map +1 -0
- package/dist/parse-gltf-header.d.ts +18 -0
- package/dist/parse-gltf-header.d.ts.map +1 -0
- package/dist/parse-gltf.d.ts +263 -0
- package/dist/parse-gltf.d.ts.map +1 -0
- package/dist/parse-skin.d.ts +70 -0
- package/dist/parse-skin.d.ts.map +1 -0
- package/dist/reimport-reuse-meta.d.ts +48 -0
- package/dist/reimport-reuse-meta.d.ts.map +1 -0
- package/dist/serialize-meta.d.ts +2 -0
- package/dist/serialize-meta.d.ts.map +1 -0
- package/dist/source-key.d.ts +44 -0
- package/dist/source-key.d.ts.map +1 -0
- package/dist/sub-asset-key.d.ts +12 -0
- package/dist/sub-asset-key.d.ts.map +1 -0
- package/dist/to-asset-pack.d.ts +2 -0
- package/dist/to-asset-pack.d.ts.map +1 -0
- package/dist/transform.d.ts +37 -0
- package/dist/transform.d.ts.map +1 -0
- package/package.json +92 -0
- package/src/__tests__/animation-target-errors.unit.test.ts +51 -0
- package/src/__tests__/bridge-material-samplers.unit.test.ts +67 -0
- package/src/__tests__/bridge-material-values.unit.test.ts +106 -0
- package/src/__tests__/bridge-mixed-color.unit.test.ts +100 -0
- package/src/__tests__/bridge-sampler-required.test-d.ts +59 -0
- package/src/__tests__/bridge-skin-shader-route.unit.test.ts +42 -0
- package/src/__tests__/bridge-skin-stride.unit.test.ts +115 -0
- package/src/__tests__/bridge-test-helpers.ts +9 -0
- package/src/__tests__/bridge.unit.test.ts +1227 -0
- package/src/__tests__/cli-gltf.integration.test.ts +122 -0
- package/src/__tests__/color-0-accessor.unit.test.ts +174 -0
- package/src/__tests__/data-uri-surface.unit.test.ts +26 -0
- package/src/__tests__/extension-admission.unit.test.ts +19 -0
- package/src/__tests__/fixtures/color-0/float-vec3.gltf +13 -0
- package/src/__tests__/fixtures/color-0/interleaved-vec3.gltf +6 -0
- package/src/__tests__/fixtures/color-0/normalized-ubyte.gltf +6 -0
- package/src/__tests__/fixtures/color-0/normalized-ushort.gltf +6 -0
- package/src/__tests__/gltf-error-derived-views.test-d.ts +152 -0
- package/src/__tests__/gltf-error-migration.test.ts +147 -0
- package/src/__tests__/gltf-error-policy-owner.unit.test.ts +185 -0
- package/src/__tests__/gltf-importer.test.ts +12 -0
- package/src/__tests__/gltf-local-artifacts.test.ts +11 -0
- package/src/__tests__/gltf.unit.test.ts +2975 -0
- package/src/__tests__/malformed-buffer-data-uri.integration.test.ts +140 -0
- package/src/__tests__/material-missing-uv.unit.test.ts +29 -0
- package/src/__tests__/material-pack-refs.integration.test.ts +58 -0
- package/src/__tests__/material-per-slot-ir.unit.test.ts +90 -0
- package/src/__tests__/material-texture-transform.unit.test.ts +105 -0
- package/src/__tests__/material-unused-uv.unit.test.ts +44 -0
- package/src/__tests__/meshopt-decode.unit.test.ts +87 -0
- package/src/__tests__/meshopt-falsifier.unit.test.ts +92 -0
- package/src/__tests__/meshopt-mode-filter-owner.test-d.ts +79 -0
- package/src/__tests__/meshopt-real-matrix.integration.test.ts +370 -0
- package/src/__tests__/morph-import.integration.test.ts +115 -0
- package/src/__tests__/morph-import.unit.test.ts +62 -0
- package/src/__tests__/parse-decode.unit.test.ts +919 -0
- package/src/__tests__/parse-gltf-multi-uv.test.ts +238 -0
- package/src/__tests__/parse-gltf-skin-attrs.unit.test.ts +191 -0
- package/src/__tests__/parse-gltf.unit.test.ts +133 -0
- package/src/__tests__/pick-e2e.integration.test.ts +238 -0
- package/src/__tests__/public-surface.unit.test.ts +10 -0
- package/src/__tests__/source-key-diagnostics.integration.test.ts +140 -0
- package/src/__tests__/source-key-producer.integration.test.ts +171 -0
- package/src/__tests__/source-key.unit.test.ts +159 -0
- package/src/accessor/decode-accessor.ts +309 -0
- package/src/accessor/decode-color.ts +128 -0
- package/src/accessor/index.ts +15 -0
- package/src/bridge.ts +928 -0
- package/src/check-extensions.ts +85 -0
- package/src/cli-gltf.ts +339 -0
- package/src/data-uri.ts +29 -0
- package/src/errors.ts +386 -0
- package/src/extract-image-bytes.ts +343 -0
- package/src/gltf-importer.ts +976 -0
- package/src/image-color-space.ts +104 -0
- package/src/importer-entry.ts +24 -0
- package/src/index.ts +148 -0
- package/src/meshopt-decode.ts +200 -0
- package/src/node-file-entry.ts +98 -0
- package/src/node-path.ts +41 -0
- package/src/parse-animation.ts +286 -0
- package/src/parse-glb-chunks.ts +167 -0
- package/src/parse-glb.ts +16 -0
- package/src/parse-gltf-header.ts +58 -0
- package/src/parse-gltf.ts +1731 -0
- package/src/parse-skin.ts +238 -0
- package/src/reimport-reuse-meta.ts +134 -0
- package/src/serialize-meta.ts +24 -0
- package/src/source-key.ts +106 -0
- package/src/sub-asset-key.ts +61 -0
- package/src/to-asset-pack.ts +8 -0
- package/src/transform.ts +117 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// extract-image-bytes.ts - resolve glTF `images[<imageIndex>]` into raw
|
|
2
|
+
// PNG / JPEG bytes (feat-20260608 M3 D-6 / requirements AC-08 + AC-09 + AC-10).
|
|
3
|
+
//
|
|
4
|
+
// The gltfImporter walks `images[]` and funnels three image source paths
|
|
5
|
+
// through a single decode seam:
|
|
6
|
+
// (a) bufferView : slice the named buffer at the bufferView range
|
|
7
|
+
// (b) data: URI : base64-decode the URI payload
|
|
8
|
+
// (c) external URI : ctx.readSibling(uri) reads the file co-located
|
|
9
|
+
// with the .gltf source
|
|
10
|
+
//
|
|
11
|
+
// This module owns the byte-extraction (no decoding); decoding is the job of
|
|
12
|
+
// ctx.decodeImage (D-1 seam: gltfImporter never imports
|
|
13
|
+
// `@forgeax/engine-image` directly). On any failure the caller produces a
|
|
14
|
+
// `gltf-image-extract-failed` GltfError carrying { imageIndex, source,
|
|
15
|
+
// reason }.
|
|
16
|
+
//
|
|
17
|
+
// Why not reuse parse-gltf.ts's buffer resolution? parseGltf.ts encapsulates
|
|
18
|
+
// buffer / bufferView decoding inside `parseGltfWithBin` and never surfaces
|
|
19
|
+
// the byte arrays. Re-running that machinery for image extraction would
|
|
20
|
+
// double the work; instead the gltfImporter parses the doc twice (parseGltf
|
|
21
|
+
// for the IR, this helper for the raw bytes) — both passes are O(file
|
|
22
|
+
// size) and the importer only runs at build time.
|
|
23
|
+
|
|
24
|
+
import type { ImportContext, ImportError as ImportErrorType } from '@forgeax/engine-types';
|
|
25
|
+
import { dataUriBase64Payload, decodeBase64 } from './data-uri.js';
|
|
26
|
+
import { parseGlbChunks } from './parse-glb-chunks.js';
|
|
27
|
+
|
|
28
|
+
/** Source of an image: matches `GltfImageExtractFailedDetail.source`. */
|
|
29
|
+
export type ImageSourceKind = 'bufferView' | 'data-uri' | 'external-uri';
|
|
30
|
+
|
|
31
|
+
/** One extracted image: bytes + the source kind that produced them. */
|
|
32
|
+
export interface ExtractedImage {
|
|
33
|
+
readonly bytes: Uint8Array;
|
|
34
|
+
readonly mimeType: 'image/png' | 'image/jpeg';
|
|
35
|
+
readonly source: ImageSourceKind;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Failure shape — the importer wraps this in a `gltf-image-extract-failed`
|
|
40
|
+
* GltfError. `source` may be undefined when the image row is malformed
|
|
41
|
+
* (no uri / no bufferView), so the importer can name 'bufferView' (most
|
|
42
|
+
* common) by default.
|
|
43
|
+
*/
|
|
44
|
+
export interface ExtractFailure {
|
|
45
|
+
readonly imageIndex: number;
|
|
46
|
+
readonly source: ImageSourceKind;
|
|
47
|
+
readonly reason: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const DATA_URI_MIME_RE = /^data:([^;,]+)(?:;[^,;]+)*;base64,/;
|
|
51
|
+
|
|
52
|
+
/** Pull the source-read-failed reason out of an ImportError without
|
|
53
|
+
* widening the discriminated union — the caller only ever sees
|
|
54
|
+
* source-read-failed here (readSibling's only failure shape). The
|
|
55
|
+
* ImportError class carries a structurally typed `.detail` field that does
|
|
56
|
+
* not auto-narrow on the `.code` discriminator, so we read `.message`
|
|
57
|
+
* which the class composes from `expected: ...; reason: ...`. */
|
|
58
|
+
function describeImportError(err: ImportErrorType): string {
|
|
59
|
+
return err.message;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Pick PNG vs JPEG from declared mimeType + magic-byte fallback. */
|
|
63
|
+
function classifyMime(
|
|
64
|
+
declaredMime: string | undefined,
|
|
65
|
+
bytes: Uint8Array,
|
|
66
|
+
): 'image/png' | 'image/jpeg' | undefined {
|
|
67
|
+
if (declaredMime === 'image/png' || declaredMime === 'image/jpeg') return declaredMime;
|
|
68
|
+
if (
|
|
69
|
+
bytes.length >= 8 &&
|
|
70
|
+
bytes[0] === 0x89 &&
|
|
71
|
+
bytes[1] === 0x50 &&
|
|
72
|
+
bytes[2] === 0x4e &&
|
|
73
|
+
bytes[3] === 0x47
|
|
74
|
+
) {
|
|
75
|
+
return 'image/png';
|
|
76
|
+
}
|
|
77
|
+
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
|
|
78
|
+
return 'image/jpeg';
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface BufferViewJsonSlim {
|
|
84
|
+
readonly buffer: number;
|
|
85
|
+
readonly byteOffset?: number;
|
|
86
|
+
readonly byteLength: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface ImageJsonSlim {
|
|
90
|
+
readonly uri?: string;
|
|
91
|
+
readonly mimeType?: string;
|
|
92
|
+
readonly bufferView?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface BufferJsonSlim {
|
|
96
|
+
readonly uri?: string;
|
|
97
|
+
readonly byteLength: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface RootJsonSlim {
|
|
101
|
+
readonly images?: readonly ImageJsonSlim[];
|
|
102
|
+
readonly bufferViews?: readonly BufferViewJsonSlim[];
|
|
103
|
+
readonly buffers?: readonly BufferJsonSlim[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Extract raw image bytes for every `images[]` row in the parsed glTF doc.
|
|
108
|
+
*
|
|
109
|
+
* @param sourceBytes the original .glb / .gltf file bytes (the GLB BIN
|
|
110
|
+
* chunk is reconstructed from these for buffer-0 lookups).
|
|
111
|
+
* @param sourcePath the meta.source path; used only for diagnostics
|
|
112
|
+
* (parseGlbChunks errors carry it through).
|
|
113
|
+
* @param ctx the ImportContext — reused for `readSibling` (external
|
|
114
|
+
* buffer .bin / external image .png/.jpg). data: URIs do not call ctx.
|
|
115
|
+
*
|
|
116
|
+
* Returns a Map<imageIndex, ExtractedImage> with one entry per resolved
|
|
117
|
+
* image. Rows that fail to extract are returned in `failures[]` so the
|
|
118
|
+
* caller can emit one `gltf-image-extract-failed` per failure (single
|
|
119
|
+
* failure does not abort the whole importer — the runner's GUID iron-law
|
|
120
|
+
* check then surfaces missing image GUIDs as a structured outage instead
|
|
121
|
+
* of the silent white-box render).
|
|
122
|
+
*/
|
|
123
|
+
export async function extractImageBytes(
|
|
124
|
+
sourceBytes: Uint8Array,
|
|
125
|
+
sourcePath: string,
|
|
126
|
+
ctx: ImportContext,
|
|
127
|
+
): Promise<{
|
|
128
|
+
readonly extracted: ReadonlyMap<number, ExtractedImage>;
|
|
129
|
+
readonly failures: readonly ExtractFailure[];
|
|
130
|
+
}> {
|
|
131
|
+
const isGlb = sourcePath.toLowerCase().endsWith('.glb');
|
|
132
|
+
let json: RootJsonSlim;
|
|
133
|
+
let glbBin: Uint8Array | undefined;
|
|
134
|
+
|
|
135
|
+
if (isGlb) {
|
|
136
|
+
const ab = sourceBytes.buffer.slice(
|
|
137
|
+
sourceBytes.byteOffset,
|
|
138
|
+
sourceBytes.byteOffset + sourceBytes.byteLength,
|
|
139
|
+
) as ArrayBuffer;
|
|
140
|
+
const chunks = parseGlbChunks(ab, sourcePath);
|
|
141
|
+
if (!chunks.ok) {
|
|
142
|
+
return {
|
|
143
|
+
extracted: new Map(),
|
|
144
|
+
failures: [
|
|
145
|
+
{
|
|
146
|
+
imageIndex: -1,
|
|
147
|
+
source: 'bufferView',
|
|
148
|
+
reason: `GLB chunk parse failed: ${chunks.error.code}`,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
json = JSON.parse(new TextDecoder().decode(chunks.value.jsonChunk)) as RootJsonSlim;
|
|
155
|
+
} catch (e) {
|
|
156
|
+
return {
|
|
157
|
+
extracted: new Map(),
|
|
158
|
+
failures: [
|
|
159
|
+
{
|
|
160
|
+
imageIndex: -1,
|
|
161
|
+
source: 'bufferView',
|
|
162
|
+
reason: `GLB JSON parse failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
glbBin = chunks.value.binChunk;
|
|
168
|
+
} else {
|
|
169
|
+
try {
|
|
170
|
+
json = JSON.parse(new TextDecoder().decode(sourceBytes)) as RootJsonSlim;
|
|
171
|
+
} catch (e) {
|
|
172
|
+
return {
|
|
173
|
+
extracted: new Map(),
|
|
174
|
+
failures: [
|
|
175
|
+
{
|
|
176
|
+
imageIndex: -1,
|
|
177
|
+
source: 'bufferView',
|
|
178
|
+
reason: `gltf JSON parse failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const images = json.images ?? [];
|
|
186
|
+
const bufferViews = json.bufferViews ?? [];
|
|
187
|
+
const buffersJson = json.buffers ?? [];
|
|
188
|
+
const extracted = new Map<number, ExtractedImage>();
|
|
189
|
+
const failures: ExtractFailure[] = [];
|
|
190
|
+
|
|
191
|
+
// Resolve buffers (lazy: only buffers actually referenced by images get fetched).
|
|
192
|
+
const buffersCache = new Map<number, Uint8Array | { readonly error: ImportErrorType | string }>();
|
|
193
|
+
async function getBuffer(bufferIndex: number): Promise<Uint8Array | { readonly error: string }> {
|
|
194
|
+
const cached = buffersCache.get(bufferIndex);
|
|
195
|
+
if (cached !== undefined) {
|
|
196
|
+
if (cached instanceof Uint8Array) return cached;
|
|
197
|
+
const e = cached.error;
|
|
198
|
+
return { error: typeof e === 'string' ? e : e.message };
|
|
199
|
+
}
|
|
200
|
+
const bufJson = buffersJson[bufferIndex];
|
|
201
|
+
if (bufJson === undefined) {
|
|
202
|
+
const reason = `buffer index ${bufferIndex} out of range (have ${buffersJson.length})`;
|
|
203
|
+
buffersCache.set(bufferIndex, { error: reason });
|
|
204
|
+
return { error: reason };
|
|
205
|
+
}
|
|
206
|
+
if (bufJson.uri === undefined) {
|
|
207
|
+
// GLB BIN chunk slot — buffer-0.
|
|
208
|
+
if (glbBin === undefined) {
|
|
209
|
+
const reason = `buffer ${bufferIndex} has no uri and no GLB BIN chunk available`;
|
|
210
|
+
buffersCache.set(bufferIndex, { error: reason });
|
|
211
|
+
return { error: reason };
|
|
212
|
+
}
|
|
213
|
+
buffersCache.set(bufferIndex, glbBin);
|
|
214
|
+
return glbBin;
|
|
215
|
+
}
|
|
216
|
+
const dataPayload = dataUriBase64Payload(bufJson.uri);
|
|
217
|
+
if (dataPayload !== undefined) {
|
|
218
|
+
try {
|
|
219
|
+
const bytes = decodeBase64(dataPayload);
|
|
220
|
+
buffersCache.set(bufferIndex, bytes);
|
|
221
|
+
return bytes;
|
|
222
|
+
} catch (e) {
|
|
223
|
+
const reason = `buffer ${bufferIndex} data URI base64 decode failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
224
|
+
buffersCache.set(bufferIndex, { error: reason });
|
|
225
|
+
return { error: reason };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const sib = await ctx.readSibling(bufJson.uri);
|
|
229
|
+
if (!sib.ok) {
|
|
230
|
+
const reason = `external buffer "${bufJson.uri}" read failed: ${describeImportError(sib.error)}`;
|
|
231
|
+
buffersCache.set(bufferIndex, { error: reason });
|
|
232
|
+
return { error: reason };
|
|
233
|
+
}
|
|
234
|
+
buffersCache.set(bufferIndex, sib.value);
|
|
235
|
+
return sib.value;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < images.length; i++) {
|
|
239
|
+
const img = images[i];
|
|
240
|
+
if (img === undefined) continue;
|
|
241
|
+
|
|
242
|
+
if (img.bufferView !== undefined) {
|
|
243
|
+
const bv = bufferViews[img.bufferView];
|
|
244
|
+
if (bv === undefined) {
|
|
245
|
+
failures.push({
|
|
246
|
+
imageIndex: i,
|
|
247
|
+
source: 'bufferView',
|
|
248
|
+
reason: `bufferView index ${img.bufferView} out of range`,
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const buf = await getBuffer(bv.buffer);
|
|
253
|
+
if (!(buf instanceof Uint8Array)) {
|
|
254
|
+
failures.push({ imageIndex: i, source: 'bufferView', reason: buf.error });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const off = bv.byteOffset ?? 0;
|
|
258
|
+
const len = bv.byteLength;
|
|
259
|
+
if (off + len > buf.byteLength) {
|
|
260
|
+
failures.push({
|
|
261
|
+
imageIndex: i,
|
|
262
|
+
source: 'bufferView',
|
|
263
|
+
reason: `bufferView ${img.bufferView} byte range [${off}..${off + len}) exceeds buffer ${bv.buffer} length ${buf.byteLength}`,
|
|
264
|
+
});
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const bytes = buf.subarray(off, off + len);
|
|
268
|
+
const mime = classifyMime(img.mimeType, bytes);
|
|
269
|
+
if (mime === undefined) {
|
|
270
|
+
failures.push({
|
|
271
|
+
imageIndex: i,
|
|
272
|
+
source: 'bufferView',
|
|
273
|
+
reason: `unsupported / unrecognised mime (declared "${img.mimeType ?? '<absent>'}", magic byte sniff failed)`,
|
|
274
|
+
});
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
extracted.set(i, { bytes: new Uint8Array(bytes), mimeType: mime, source: 'bufferView' });
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (img.uri !== undefined) {
|
|
282
|
+
const dataPayload = dataUriBase64Payload(img.uri);
|
|
283
|
+
if (dataPayload !== undefined) {
|
|
284
|
+
let bytes: Uint8Array;
|
|
285
|
+
try {
|
|
286
|
+
bytes = decodeBase64(dataPayload);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
failures.push({
|
|
289
|
+
imageIndex: i,
|
|
290
|
+
source: 'data-uri',
|
|
291
|
+
reason: `base64 decode failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
292
|
+
});
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const mimeMatch = DATA_URI_MIME_RE.exec(img.uri);
|
|
296
|
+
const declaredMime = mimeMatch?.[1] ?? img.mimeType;
|
|
297
|
+
const mime = classifyMime(declaredMime, bytes);
|
|
298
|
+
if (mime === undefined) {
|
|
299
|
+
failures.push({
|
|
300
|
+
imageIndex: i,
|
|
301
|
+
source: 'data-uri',
|
|
302
|
+
reason: `data: URI mime "${declaredMime ?? '<absent>'}" not png/jpeg`,
|
|
303
|
+
});
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
extracted.set(i, { bytes, mimeType: mime, source: 'data-uri' });
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const sib = await ctx.readSibling(img.uri);
|
|
310
|
+
if (!sib.ok) {
|
|
311
|
+
failures.push({
|
|
312
|
+
imageIndex: i,
|
|
313
|
+
source: 'external-uri',
|
|
314
|
+
reason: `external URI "${img.uri}" read failed: ${describeImportError(sib.error)}`,
|
|
315
|
+
});
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const mime = classifyMime(img.mimeType, sib.value);
|
|
319
|
+
if (mime === undefined) {
|
|
320
|
+
failures.push({
|
|
321
|
+
imageIndex: i,
|
|
322
|
+
source: 'external-uri',
|
|
323
|
+
reason: `external URI "${img.uri}" mime not png/jpeg (declared "${img.mimeType ?? '<absent>'}", magic byte sniff failed)`,
|
|
324
|
+
});
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
extracted.set(i, {
|
|
328
|
+
bytes: new Uint8Array(sib.value),
|
|
329
|
+
mimeType: mime,
|
|
330
|
+
source: 'external-uri',
|
|
331
|
+
});
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
failures.push({
|
|
336
|
+
imageIndex: i,
|
|
337
|
+
source: 'bufferView',
|
|
338
|
+
reason: 'image row has neither bufferView nor uri',
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return { extracted, failures };
|
|
343
|
+
}
|