usd-gltf 0.1.0 → 0.2.0
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 +23 -3
- package/package.json +1 -1
- package/src/convert/gltf-to-usd.js +166 -46
- package/src/convert/usd-material.js +7 -2
- package/src/convert/usd-to-gltf.js +8 -4
- package/src/gltf/document.js +107 -18
- package/src/gltf/glb.js +2 -2
- package/src/image/index.js +52 -1
- package/src/image/inflate.js +14 -12
- package/src/image/png.js +1 -1
- package/src/index.d.ts +25 -4
- package/src/index.js +33 -14
- package/src/limits.js +101 -0
- package/src/usd/stage.js +13 -9
- package/src/usd/usda/writer.js +16 -1
- package/src/usd/usdc/reader.js +63 -13
- package/src/usd/usdc/writer.js +53 -3
- package/src/usd/usdz.js +9 -3
package/src/gltf/document.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { readGLB, writeGLB } from './glb.js';
|
|
4
4
|
import { decodeMeshoptView } from './meshopt.js';
|
|
5
|
+
import { Budget, checkSize } from '../limits.js';
|
|
5
6
|
|
|
6
7
|
const COMPONENT_ARRAYS = {
|
|
7
8
|
5120: Int8Array,
|
|
@@ -14,6 +15,43 @@ const COMPONENT_ARRAYS = {
|
|
|
14
15
|
|
|
15
16
|
export const TYPE_SIZES = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 };
|
|
16
17
|
|
|
18
|
+
// Extensions an asset may require. Material extensions without a UsdPreviewSurface equivalent are
|
|
19
|
+
// approximated and kept on the USD material for the way back, so every KHR_materials_* is accepted.
|
|
20
|
+
const SUPPORTED_EXTENSIONS = new Set( [
|
|
21
|
+
'EXT_mesh_gpu_instancing', 'EXT_meshopt_compression', 'KHR_meshopt_compression', 'KHR_mesh_quantization',
|
|
22
|
+
'EXT_texture_webp', 'EXT_texture_avif', 'KHR_texture_transform', 'KHR_animation_pointer', 'KHR_lights_punctual',
|
|
23
|
+
'KHR_node_visibility', 'KHR_xmp_json_ld',
|
|
24
|
+
] );
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The asset's required extensions that can't be converted (e.g. KHR_draco_mesh_compression, KHR_texture_basisu).
|
|
28
|
+
* @param {object} json
|
|
29
|
+
* @returns {string[]}
|
|
30
|
+
*/
|
|
31
|
+
export function unsupportedExtensions( json ) {
|
|
32
|
+
|
|
33
|
+
return ( json.extensionsRequired || [] ).filter( ( name ) => ! SUPPORTED_EXTENSIONS.has( name ) && ! name.startsWith( 'KHR_materials_' ) );
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Optional Draco needs uncompressed fallback data: without a bufferView or sparse values the
|
|
38
|
+
// accessors would read as zeros.
|
|
39
|
+
function checkDracoFallbacks( json ) {
|
|
40
|
+
|
|
41
|
+
( json.meshes || [] ).forEach( ( mesh, m ) => mesh.primitives.forEach( ( primitive, p ) => {
|
|
42
|
+
|
|
43
|
+
if ( ! primitive.extensions?.KHR_draco_mesh_compression ) return;
|
|
44
|
+
const used = [ ...Object.values( primitive.attributes ), primitive.indices ].filter( ( a ) => a !== undefined );
|
|
45
|
+
if ( used.some( ( a ) => json.accessors[ a ].bufferView === undefined && ! json.accessors[ a ].sparse ) ) {
|
|
46
|
+
|
|
47
|
+
throw new Error( `usd-gltf: mesh ${ m } primitive ${ p } is Draco compressed with no uncompressed fallback; KHR_draco_mesh_compression is not supported` );
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
} ) );
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
|
|
17
55
|
function decodeDataURI( uri ) {
|
|
18
56
|
|
|
19
57
|
const comma = uri.indexOf( ',' );
|
|
@@ -38,11 +76,12 @@ function decodeDataURI( uri ) {
|
|
|
38
76
|
*/
|
|
39
77
|
export class GLTFDocument {
|
|
40
78
|
|
|
41
|
-
constructor( json, buffers, images ) {
|
|
79
|
+
constructor( json, buffers, images, budget = new Budget() ) {
|
|
42
80
|
|
|
43
81
|
this.json = json;
|
|
44
82
|
this.buffers = buffers; // Uint8Array per json.buffers entry
|
|
45
83
|
this.images = images; // { bytes, mimeType, name } per json.images entry
|
|
84
|
+
this.budget = budget;
|
|
46
85
|
this.accessorCache = new Map();
|
|
47
86
|
this.decodedViews = new Map(); // bufferView index -> bytes, for compressed views
|
|
48
87
|
|
|
@@ -52,9 +91,11 @@ export class GLTFDocument {
|
|
|
52
91
|
|
|
53
92
|
const decoded = this.decodedViews.get( index );
|
|
54
93
|
if ( decoded ) return decoded;
|
|
55
|
-
const view = this.json.bufferViews[ index ];
|
|
56
|
-
const buffer = this.buffers[ view.buffer ];
|
|
57
|
-
|
|
94
|
+
const view = this.json.bufferViews?.[ index ];
|
|
95
|
+
const buffer = view && this.buffers[ view.buffer ];
|
|
96
|
+
const start = view?.byteOffset || 0;
|
|
97
|
+
if ( ! buffer || ! ( start + view.byteLength <= buffer.length ) ) throw new Error( `usd-gltf: bufferView ${ index } is outside its buffer` );
|
|
98
|
+
return buffer.subarray( start, start + view.byteLength );
|
|
58
99
|
|
|
59
100
|
}
|
|
60
101
|
|
|
@@ -68,11 +109,16 @@ export class GLTFDocument {
|
|
|
68
109
|
const key = index + ( normalize ? 'n' : '' );
|
|
69
110
|
if ( this.accessorCache.has( key ) ) return this.accessorCache.get( key );
|
|
70
111
|
|
|
71
|
-
const accessor = this.json.accessors[ index ];
|
|
72
|
-
const ArrayType = COMPONENT_ARRAYS[ accessor
|
|
73
|
-
const itemSize = TYPE_SIZES[ accessor
|
|
112
|
+
const accessor = this.json.accessors?.[ index ];
|
|
113
|
+
const ArrayType = COMPONENT_ARRAYS[ accessor?.componentType ];
|
|
114
|
+
const itemSize = TYPE_SIZES[ accessor?.type ];
|
|
115
|
+
if ( ! ArrayType || ! itemSize ) throw new Error( `usd-gltf: accessor ${ index } is missing or has an invalid type` );
|
|
74
116
|
const count = accessor.count;
|
|
75
|
-
|
|
117
|
+
const stage = `accessor ${ index }`;
|
|
118
|
+
const size = count * itemSize * ArrayType.BYTES_PER_ELEMENT;
|
|
119
|
+
checkSize( size, stage );
|
|
120
|
+
|
|
121
|
+
let read = null;
|
|
76
122
|
|
|
77
123
|
if ( accessor.bufferView !== undefined ) {
|
|
78
124
|
|
|
@@ -88,6 +134,17 @@ export class GLTFDocument {
|
|
|
88
134
|
const columnStride = Math.ceil( rows * elementBytes / 4 ) * 4;
|
|
89
135
|
const packed = columns === 1 ? itemSize * elementBytes : columnStride * columns;
|
|
90
136
|
const stride = view.byteStride || packed;
|
|
137
|
+
if ( count > 0 && ! ( offset + stride * ( count - 1 ) + packed <= bytes.length ) ) throw new Error( `usd-gltf: ${ stage } reads past the end of its bufferView` );
|
|
138
|
+
read = { view, bytes, elementBytes, offset, columns, rows, columnStride, stride };
|
|
139
|
+
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.budget.decode( size, stage );
|
|
143
|
+
let array = new ArrayType( count * itemSize );
|
|
144
|
+
|
|
145
|
+
if ( read ) {
|
|
146
|
+
|
|
147
|
+
const { bytes, elementBytes, offset, columns, rows, columnStride, stride } = read;
|
|
91
148
|
|
|
92
149
|
const dv = new DataView( bytes.buffer, bytes.byteOffset, bytes.byteLength );
|
|
93
150
|
const getters = { 5120: 'getInt8', 5121: 'getUint8', 5122: 'getInt16', 5123: 'getUint16', 5125: 'getUint32', 5126: 'getFloat32' };
|
|
@@ -120,9 +177,16 @@ export class GLTFDocument {
|
|
|
120
177
|
const IndexType = COMPONENT_ARRAYS[ indices.componentType ];
|
|
121
178
|
const ib = this.bufferViewBytes( indices.bufferView );
|
|
122
179
|
const vb = this.bufferViewBytes( values.bufferView );
|
|
180
|
+
if ( ! IndexType || ! Number.isSafeInteger( n ) || n < 0 || n > count ) throw new Error( `usd-gltf: ${ stage } has an invalid sparse count` );
|
|
181
|
+
if ( ! ( ( indices.byteOffset || 0 ) + n * IndexType.BYTES_PER_ELEMENT <= ib.length && ( values.byteOffset || 0 ) + n * itemSize * ArrayType.BYTES_PER_ELEMENT <= vb.length ) ) throw new Error( `usd-gltf: ${ stage } sparse data is outside its bufferViews` );
|
|
123
182
|
const idx = new IndexType( ib.slice( indices.byteOffset || 0, ( indices.byteOffset || 0 ) + n * IndexType.BYTES_PER_ELEMENT ).buffer );
|
|
124
183
|
const vals = new ArrayType( vb.slice( values.byteOffset || 0, ( values.byteOffset || 0 ) + n * itemSize * ArrayType.BYTES_PER_ELEMENT ).buffer );
|
|
125
|
-
for ( let i = 0; i < n; i ++ )
|
|
184
|
+
for ( let i = 0; i < n; i ++ ) {
|
|
185
|
+
|
|
186
|
+
if ( idx[ i ] >= count ) throw new Error( `usd-gltf: ${ stage } sparse index is out of range` );
|
|
187
|
+
for ( let c = 0; c < itemSize; c ++ ) array[ idx[ i ] * itemSize + c ] = vals[ i * itemSize + c ];
|
|
188
|
+
|
|
189
|
+
}
|
|
126
190
|
|
|
127
191
|
}
|
|
128
192
|
|
|
@@ -130,6 +194,7 @@ export class GLTFDocument {
|
|
|
130
194
|
|
|
131
195
|
if ( normalize && normalized ) {
|
|
132
196
|
|
|
197
|
+
this.budget.decode( array.length * 4, stage );
|
|
133
198
|
const out = new Float32Array( array.length );
|
|
134
199
|
const ct = accessor.componentType;
|
|
135
200
|
for ( let i = 0; i < array.length; i ++ ) {
|
|
@@ -154,7 +219,9 @@ export class GLTFDocument {
|
|
|
154
219
|
getFloats( index ) {
|
|
155
220
|
|
|
156
221
|
const { array, itemSize, count } = this.getAccessor( index );
|
|
157
|
-
|
|
222
|
+
if ( array instanceof Float32Array ) return { array, itemSize, count };
|
|
223
|
+
this.budget.decode( array.length * 4, `accessor ${ index }` );
|
|
224
|
+
return { array: Float32Array.from( array ), itemSize, count };
|
|
158
225
|
|
|
159
226
|
}
|
|
160
227
|
|
|
@@ -165,13 +232,18 @@ export class GLTFDocument {
|
|
|
165
232
|
* @param {Uint8Array} bytes
|
|
166
233
|
* @param {{ resolver?: ( uri: string ) => Promise<Uint8Array>|Uint8Array }} [options] loads external uris (already decoded from URI escaping)
|
|
167
234
|
*/
|
|
168
|
-
export async function readGLTF( bytes, { resolver = null } = {} ) {
|
|
235
|
+
export async function readGLTF( bytes, { resolver = null, budget = new Budget() } = {} ) {
|
|
169
236
|
|
|
170
237
|
let json, bin = null;
|
|
238
|
+
bytes = plainBytes( bytes );
|
|
171
239
|
|
|
172
240
|
if ( bytes[ 0 ] === 0x67 && bytes[ 1 ] === 0x6C && bytes[ 2 ] === 0x54 && bytes[ 3 ] === 0x46 ) ( { json, bin } = readGLB( bytes ) );
|
|
173
241
|
else json = JSON.parse( new TextDecoder().decode( bytes ).replace( /^/, '' ) );
|
|
174
242
|
|
|
243
|
+
const unsupported = unsupportedExtensions( json );
|
|
244
|
+
if ( unsupported.length ) throw new Error( `usd-gltf: required extension${ unsupported.length > 1 ? 's' : '' } ${ unsupported.join( ', ' ) } not supported` );
|
|
245
|
+
checkDracoFallbacks( json );
|
|
246
|
+
|
|
175
247
|
async function load( uri ) {
|
|
176
248
|
|
|
177
249
|
if ( uri.startsWith( 'data:' ) ) return decodeDataURI( uri ).bytes;
|
|
@@ -179,7 +251,7 @@ export async function readGLTF( bytes, { resolver = null } = {} ) {
|
|
|
179
251
|
let path = uri;
|
|
180
252
|
try { path = decodeURIComponent( uri ); } catch { /* keep as is */ }
|
|
181
253
|
const data = await resolver( path );
|
|
182
|
-
return
|
|
254
|
+
return plainBytes( data );
|
|
183
255
|
|
|
184
256
|
}
|
|
185
257
|
|
|
@@ -188,8 +260,9 @@ export async function readGLTF( bytes, { resolver = null } = {} ) {
|
|
|
188
260
|
if ( buffer.uri === undefined ) {
|
|
189
261
|
|
|
190
262
|
if ( i === 0 && bin ) return bin;
|
|
191
|
-
// e.g. EXT_meshopt_compression fallback buffers
|
|
192
|
-
|
|
263
|
+
// e.g. EXT_meshopt_compression fallback buffers: only read through decoded views, so they
|
|
264
|
+
// need no storage, and any other view into one fails its bounds check
|
|
265
|
+
return new Uint8Array( 0 );
|
|
193
266
|
|
|
194
267
|
}
|
|
195
268
|
|
|
@@ -197,12 +270,16 @@ export async function readGLTF( bytes, { resolver = null } = {} ) {
|
|
|
197
270
|
|
|
198
271
|
} ) );
|
|
199
272
|
|
|
200
|
-
const doc = new GLTFDocument( json, buffers, [] );
|
|
273
|
+
const doc = new GLTFDocument( json, buffers, [], budget );
|
|
201
274
|
|
|
202
275
|
( json.bufferViews || [] ).forEach( ( view, i ) => {
|
|
203
276
|
|
|
204
277
|
const ext = view.extensions?.KHR_meshopt_compression || view.extensions?.EXT_meshopt_compression;
|
|
205
|
-
if (
|
|
278
|
+
if ( ! ext ) return;
|
|
279
|
+
const start = ext.byteOffset || 0;
|
|
280
|
+
if ( ! buffers[ ext.buffer ] || ! ( start + ext.byteLength <= buffers[ ext.buffer ].length ) ) throw new Error( `usd-gltf: bufferView ${ i } compressed data is outside its buffer` );
|
|
281
|
+
budget.decode( ext.count * ext.byteStride, `bufferView ${ i }` );
|
|
282
|
+
doc.decodedViews.set( i, decodeMeshoptView( ext, buffers ) );
|
|
206
283
|
|
|
207
284
|
} );
|
|
208
285
|
|
|
@@ -228,6 +305,14 @@ export async function readGLTF( bytes, { resolver = null } = {} ) {
|
|
|
228
305
|
|
|
229
306
|
}
|
|
230
307
|
|
|
308
|
+
// A plain Uint8Array view: a Node Buffer's slice() aliases its memory instead of copying, so data
|
|
309
|
+
// read through .slice().buffer would be the wrong bytes.
|
|
310
|
+
function plainBytes( data ) {
|
|
311
|
+
|
|
312
|
+
return ArrayBuffer.isView( data ) ? new Uint8Array( data.buffer, data.byteOffset, data.byteLength ) : new Uint8Array( data );
|
|
313
|
+
|
|
314
|
+
}
|
|
315
|
+
|
|
231
316
|
function guessMime( uri ) {
|
|
232
317
|
|
|
233
318
|
const ext = uri.split( '?' )[ 0 ].split( '.' ).pop().toLowerCase();
|
|
@@ -240,8 +325,9 @@ function guessMime( uri ) {
|
|
|
240
325
|
*/
|
|
241
326
|
export class GLTFBuilder {
|
|
242
327
|
|
|
243
|
-
constructor( generator = 'usd-gltf' ) {
|
|
328
|
+
constructor( generator = 'usd-gltf', budget = new Budget() ) {
|
|
244
329
|
|
|
330
|
+
this.budget = budget;
|
|
245
331
|
this.json = { asset: { version: '2.0', generator }, scenes: [], nodes: [], meshes: [], materials: [], textures: [], images: [], samplers: [], accessors: [], bufferViews: [], buffers: [] };
|
|
246
332
|
this.chunks = [];
|
|
247
333
|
this.byteLength = 0;
|
|
@@ -262,6 +348,7 @@ export class GLTFBuilder {
|
|
|
262
348
|
const pad = ( 4 - ( this.byteLength % 4 ) ) % 4;
|
|
263
349
|
if ( pad ) { this.chunks.push( new Uint8Array( pad ) ); this.byteLength += pad; }
|
|
264
350
|
|
|
351
|
+
this.budget.output( this.byteLength + bytes.byteLength, 'glb buffer' );
|
|
265
352
|
const view = { buffer: 0, byteOffset: this.byteLength, byteLength: bytes.byteLength };
|
|
266
353
|
if ( target ) view.target = target;
|
|
267
354
|
this.chunks.push( bytes instanceof Uint8Array ? bytes : new Uint8Array( bytes.buffer, bytes.byteOffset, bytes.byteLength ) );
|
|
@@ -323,7 +410,9 @@ export class GLTFBuilder {
|
|
|
323
410
|
|
|
324
411
|
toGLB() {
|
|
325
412
|
|
|
326
|
-
const json = this.finalize();
|
|
413
|
+
const json = new TextEncoder().encode( JSON.stringify( this.finalize() ) );
|
|
414
|
+
const pad4 = ( n ) => Math.ceil( n / 4 ) * 4;
|
|
415
|
+
this.budget.output( 20 + pad4( json.byteLength ) + ( this.byteLength ? 8 + pad4( this.byteLength ) : 0 ), 'glb' );
|
|
327
416
|
const bin = new Uint8Array( this.byteLength );
|
|
328
417
|
let offset = 0;
|
|
329
418
|
for ( const chunk of this.chunks ) {
|
package/src/gltf/glb.js
CHANGED
|
@@ -68,13 +68,13 @@ export function readGLB( data ) {
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Packs glTF JSON and an optional binary buffer into a GLB.
|
|
71
|
-
* @param {object} json
|
|
71
|
+
* @param {object|Uint8Array} json the glTF JSON, or it already encoded as UTF-8
|
|
72
72
|
* @param {Uint8Array|null} [bin]
|
|
73
73
|
* @returns {Uint8Array}
|
|
74
74
|
*/
|
|
75
75
|
export function writeGLB( json, bin = null ) {
|
|
76
76
|
|
|
77
|
-
const jsonBytes = encoder.encode( JSON.stringify( json ) );
|
|
77
|
+
const jsonBytes = json instanceof Uint8Array ? json : encoder.encode( JSON.stringify( json ) );
|
|
78
78
|
const jsonLength = pad4( jsonBytes.byteLength );
|
|
79
79
|
const binLength = bin ? pad4( bin.byteLength ) : 0;
|
|
80
80
|
|
package/src/image/index.js
CHANGED
|
@@ -65,6 +65,54 @@ export function isGreyscaleImage( bytes ) {
|
|
|
65
65
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Width, height and bytes per pixel of a PNG, JPEG or WebP from its header, or null.
|
|
70
|
+
* @param {Uint8Array} bytes
|
|
71
|
+
* @returns {{ width: number, height: number, pixelBytes: number }|null}
|
|
72
|
+
*/
|
|
73
|
+
export function imageSize( bytes ) {
|
|
74
|
+
|
|
75
|
+
bytes = toBytes( bytes );
|
|
76
|
+
const type = imageMimeType( bytes );
|
|
77
|
+
const be16 = ( o ) => ( bytes[ o ] << 8 ) | bytes[ o + 1 ];
|
|
78
|
+
const le16 = ( o ) => bytes[ o ] | ( bytes[ o + 1 ] << 8 );
|
|
79
|
+
const le24 = ( o ) => le16( o ) | ( bytes[ o + 2 ] << 16 );
|
|
80
|
+
|
|
81
|
+
if ( type === 'image/png' && bytes.length >= 26 ) {
|
|
82
|
+
|
|
83
|
+
const width = ( be16( 16 ) * 65536 ) + be16( 18 ), height = ( be16( 20 ) * 65536 ) + be16( 22 );
|
|
84
|
+
return { width, height, pixelBytes: bytes[ 24 ] === 16 ? 8 : 4 };
|
|
85
|
+
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if ( type === 'image/jpeg' ) {
|
|
89
|
+
|
|
90
|
+
for ( let o = 2; o + 9 < bytes.length; ) {
|
|
91
|
+
|
|
92
|
+
if ( bytes[ o ] !== 0xFF ) return null;
|
|
93
|
+
const marker = bytes[ o + 1 ];
|
|
94
|
+
if ( marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8 && marker !== 0xCC ) return { width: be16( o + 7 ), height: be16( o + 5 ), pixelBytes: 4 };
|
|
95
|
+
o += 2 + be16( o + 2 );
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if ( type === 'image/webp' && bytes.length >= 30 ) {
|
|
104
|
+
|
|
105
|
+
const chunk = String.fromCharCode( bytes[ 12 ], bytes[ 13 ], bytes[ 14 ], bytes[ 15 ] );
|
|
106
|
+
if ( chunk === 'VP8X' ) return { width: le24( 24 ) + 1, height: le24( 27 ) + 1, pixelBytes: 4 };
|
|
107
|
+
if ( chunk === 'VP8 ' ) return { width: le16( 26 ) & 0x3FFF, height: le16( 28 ) & 0x3FFF, pixelBytes: 4 };
|
|
108
|
+
if ( chunk === 'VP8L' ) { const b = bytes[ 21 ] | ( bytes[ 22 ] << 8 ) | ( bytes[ 23 ] << 16 ) | ( bytes[ 24 ] << 24 ); return { width: ( b & 0x3FFF ) + 1, height: ( ( b >>> 14 ) & 0x3FFF ) + 1, pixelBytes: 4 }; }
|
|
109
|
+
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
|
|
114
|
+
}
|
|
115
|
+
|
|
68
116
|
/**
|
|
69
117
|
* Decodes a PNG, JPEG or WebP. The format is sniffed from the bytes; mimeType is only a fallback.
|
|
70
118
|
* @param {Uint8Array|ArrayBuffer} bytes
|
|
@@ -141,10 +189,13 @@ export async function encodePNGAsync( image ) {
|
|
|
141
189
|
* @param {Uint8Array} bytes
|
|
142
190
|
* @param {string} [mimeType]
|
|
143
191
|
* @param {( bytes: Uint8Array, mimeType?: string ) => Promise<object>|object} [decoder] overrides both
|
|
192
|
+
* @param {import('../limits.js').Budget} [budget] charged with the decoded size, from the header, before decoding
|
|
144
193
|
*/
|
|
145
|
-
export async function decodeImageAsync( bytes, mimeType, decoder = null ) {
|
|
194
|
+
export async function decodeImageAsync( bytes, mimeType, decoder = null, budget = null ) {
|
|
146
195
|
|
|
147
196
|
bytes = toBytes( bytes );
|
|
197
|
+
const size = budget && imageSize( bytes );
|
|
198
|
+
if ( size ) budget.decode( size.width * size.height * size.pixelBytes, 'image' );
|
|
148
199
|
if ( decoder ) return decoder( bytes, mimeType );
|
|
149
200
|
const type = imageMimeType( bytes ) || mimeType;
|
|
150
201
|
|
package/src/image/inflate.js
CHANGED
|
@@ -137,12 +137,12 @@ function corrupt( what ) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// Inflates the raw DEFLATE stream starting at src[ pos ]. Returns the output and the byte offset just past the stream.
|
|
140
|
-
function inflateStream( src, pos, sizeHint ) {
|
|
140
|
+
function inflateStream( src, pos, sizeHint, maxSize ) {
|
|
141
141
|
|
|
142
142
|
const srcLen = src.length;
|
|
143
143
|
const overrun = srcLen + 8;
|
|
144
144
|
|
|
145
|
-
let out = new Uint8Array( sizeHint > 0 ? sizeHint : Math.max( 1 << 15, srcLen * 4 ) );
|
|
145
|
+
let out = new Uint8Array( Math.min( sizeHint > 0 ? sizeHint : Math.max( 1 << 15, srcLen * 4 ), maxSize ) );
|
|
146
146
|
let op = 0;
|
|
147
147
|
|
|
148
148
|
let bitBuf = 0;
|
|
@@ -154,7 +154,7 @@ function inflateStream( src, pos, sizeHint ) {
|
|
|
154
154
|
let clTable = null;
|
|
155
155
|
|
|
156
156
|
let final = 0;
|
|
157
|
-
const state = { src, overrun, pos, bitBuf, bitCnt, out, op };
|
|
157
|
+
const state = { src, overrun, pos, bitBuf, bitCnt, out, op, maxSize };
|
|
158
158
|
|
|
159
159
|
do {
|
|
160
160
|
|
|
@@ -188,7 +188,7 @@ function inflateStream( src, pos, sizeHint ) {
|
|
|
188
188
|
pos += 4;
|
|
189
189
|
if ( pos + len > srcLen ) throw corrupt( 'unexpected end' );
|
|
190
190
|
|
|
191
|
-
if ( op + len > out.length ) out = grow( out, op + len );
|
|
191
|
+
if ( op + len > out.length ) out = grow( out, op + len, maxSize );
|
|
192
192
|
out.set( src.subarray( pos, pos + len ), op );
|
|
193
193
|
op += len;
|
|
194
194
|
pos += len;
|
|
@@ -371,7 +371,7 @@ function decodeSymbols( state, litTable, litMask, distTable, distMask ) {
|
|
|
371
371
|
|
|
372
372
|
if ( op === outLen ) {
|
|
373
373
|
|
|
374
|
-
out = grow( out, op + 1 );
|
|
374
|
+
out = grow( out, op + 1, state.maxSize );
|
|
375
375
|
outLen = out.length;
|
|
376
376
|
|
|
377
377
|
}
|
|
@@ -435,7 +435,7 @@ function decodeSymbols( state, litTable, litMask, distTable, distMask ) {
|
|
|
435
435
|
|
|
436
436
|
if ( op + len > outLen ) {
|
|
437
437
|
|
|
438
|
-
out = grow( out, op + len );
|
|
438
|
+
out = grow( out, op + len, state.maxSize );
|
|
439
439
|
outLen = out.length;
|
|
440
440
|
|
|
441
441
|
}
|
|
@@ -464,9 +464,10 @@ function decodeSymbols( state, litTable, litMask, distTable, distMask ) {
|
|
|
464
464
|
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
-
function grow( out, need ) {
|
|
467
|
+
function grow( out, need, maxSize ) {
|
|
468
468
|
|
|
469
|
-
|
|
469
|
+
if ( need > maxSize ) throw corrupt( `more than the expected ${ maxSize } bytes` );
|
|
470
|
+
const next = new Uint8Array( Math.min( Math.max( need, out.length * 2 ), maxSize ) );
|
|
470
471
|
next.set( out );
|
|
471
472
|
return next;
|
|
472
473
|
|
|
@@ -482,11 +483,12 @@ function toBytes( data ) {
|
|
|
482
483
|
* Decompresses raw DEFLATE data.
|
|
483
484
|
* @param {Uint8Array} bytes
|
|
484
485
|
* @param {number} [sizeHint] expected output size, avoids reallocation when known
|
|
486
|
+
* @param {number} [maxSize] fails rather than produce more than this
|
|
485
487
|
* @returns {Uint8Array}
|
|
486
488
|
*/
|
|
487
|
-
export function inflate( bytes, sizeHint = 0 ) {
|
|
489
|
+
export function inflate( bytes, sizeHint = 0, maxSize = Infinity ) {
|
|
488
490
|
|
|
489
|
-
return inflateStream( toBytes( bytes ), 0, sizeHint ).data;
|
|
491
|
+
return inflateStream( toBytes( bytes ), 0, sizeHint, maxSize ).data;
|
|
490
492
|
|
|
491
493
|
}
|
|
492
494
|
|
|
@@ -496,7 +498,7 @@ export function inflate( bytes, sizeHint = 0 ) {
|
|
|
496
498
|
* @param {number} [sizeHint]
|
|
497
499
|
* @returns {Uint8Array}
|
|
498
500
|
*/
|
|
499
|
-
export function inflateZlib( bytes, sizeHint = 0 ) {
|
|
501
|
+
export function inflateZlib( bytes, sizeHint = 0, maxSize = Infinity ) {
|
|
500
502
|
|
|
501
503
|
bytes = toBytes( bytes );
|
|
502
504
|
if ( bytes.length < 2 ) throw corrupt( 'missing zlib header' );
|
|
@@ -506,7 +508,7 @@ export function inflateZlib( bytes, sizeHint = 0 ) {
|
|
|
506
508
|
if ( ( cmf & 15 ) !== 8 || ( cmf >> 4 ) > 7 || ( ( cmf << 8 ) | flg ) % 31 !== 0 ) throw corrupt( 'bad zlib header' );
|
|
507
509
|
if ( flg & 32 ) throw corrupt( 'zlib preset dictionary not supported' );
|
|
508
510
|
|
|
509
|
-
const { data, end } = inflateStream( bytes, 2, sizeHint );
|
|
511
|
+
const { data, end } = inflateStream( bytes, 2, sizeHint, maxSize );
|
|
510
512
|
|
|
511
513
|
if ( end + 4 <= bytes.length ) {
|
|
512
514
|
|
package/src/image/png.js
CHANGED
|
@@ -219,7 +219,7 @@ export function decodePNG( bytes ) {
|
|
|
219
219
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
const raw = inflateZlib( zdata, expected );
|
|
222
|
+
const raw = inflateZlib( zdata, expected, expected * 2 + 1024 ); // tolerates trailing data, not a decompression bomb
|
|
223
223
|
if ( raw.length < expected ) throw new Error( 'usd-gltf: PNG image data is truncated' );
|
|
224
224
|
|
|
225
225
|
const channels = colorType === 3 ? ( trns !== null ? 4 : 3 ) : samples + ( trns !== null ? 1 : 0 );
|
package/src/index.d.ts
CHANGED
|
@@ -13,7 +13,28 @@ export interface DecodedImage {
|
|
|
13
13
|
/** Replaces the built in image decoding (the platform decoder in browsers, JavaScript decoders in Node). */
|
|
14
14
|
export type ImageDecoder = ( bytes: Uint8Array, mimeType?: string ) => DecodedImage | Promise<DecodedImage>;
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/** Opt-in resource limits, for both directions. See README "Limits". */
|
|
17
|
+
export interface ConversionLimits {
|
|
18
|
+
/** Decoded data held during the conversion: accessors, USD arrays, decompressed data, images, arrays written to the output layer. */
|
|
19
|
+
maxDecodedBytes?: number;
|
|
20
|
+
/** Largest output buffer: checked as writers grow and before the final usdz or glb is allocated. */
|
|
21
|
+
maxOutputBytes?: number;
|
|
22
|
+
/** Cancels the conversion, which then rejects with the signal's reason. */
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Thrown when a conversion would go over one of its limits. Carries sizes only, never input data or file names. */
|
|
27
|
+
export class ConversionLimitError extends Error {
|
|
28
|
+
name: 'ConversionLimitError';
|
|
29
|
+
code: 'USD_GLTF_LIMIT';
|
|
30
|
+
/** Where the limit was reached, e.g. 'accessor 3', 'usdc', 'image', 'glb'. */
|
|
31
|
+
stage: string;
|
|
32
|
+
resource: 'decoded' | 'output';
|
|
33
|
+
limit: number;
|
|
34
|
+
attempted: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface USDToGLBOptions extends ConversionLimits {
|
|
17
38
|
/** Name of the input file, used to resolve relative paths. */
|
|
18
39
|
identifier?: string;
|
|
19
40
|
/** Needed when the USD references files outside a usdz package. */
|
|
@@ -27,7 +48,7 @@ export interface USDToGLBOptions {
|
|
|
27
48
|
log?: ( message: string ) => void;
|
|
28
49
|
}
|
|
29
50
|
|
|
30
|
-
export interface GLBToUSDOptions {
|
|
51
|
+
export interface GLBToUSDOptions extends ConversionLimits {
|
|
31
52
|
/** Output encoding: a usdz package (default), or a single usda / usdc layer without its textures. */
|
|
32
53
|
format?: 'usdz' | 'usda' | 'usdc';
|
|
33
54
|
/** Needed for .gltf files with external buffers or images. */
|
|
@@ -51,8 +72,8 @@ export function detectFormat( data: Uint8Array | ArrayBuffer ): 'glb' | 'gltf' |
|
|
|
51
72
|
/** Splits a GLB into its JSON and binary chunk. */
|
|
52
73
|
export function readGLB( data: Uint8Array | ArrayBuffer ): { json: any, bin: Uint8Array | null };
|
|
53
74
|
|
|
54
|
-
/** Packs glTF JSON and a binary buffer into a GLB. */
|
|
55
|
-
export function writeGLB( json: object, bin?: Uint8Array | null ): Uint8Array;
|
|
75
|
+
/** Packs glTF JSON (an object, or already UTF-8 encoded) and a binary buffer into a GLB. */
|
|
76
|
+
export function writeGLB( json: object | Uint8Array, bin?: Uint8Array | null ): Uint8Array;
|
|
56
77
|
|
|
57
78
|
/** The uncomposed contents of one USD layer; see src/usd/model.js for the value representation. */
|
|
58
79
|
export interface Layer {
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readGLTF } from './gltf/document.js';
|
|
|
5
5
|
import { writeUSDA } from './usd/usda/writer.js';
|
|
6
6
|
import { writeUSDC } from './usd/usdc/writer.js';
|
|
7
7
|
import { writeUSDZ } from './usd/usdz.js';
|
|
8
|
+
import { Budget } from './limits.js';
|
|
8
9
|
|
|
9
10
|
export { readGLB, writeGLB } from './gltf/glb.js';
|
|
10
11
|
export { detectFormat } from './detect.js';
|
|
@@ -13,6 +14,7 @@ export { readUSDA } from './usd/usda/reader.js';
|
|
|
13
14
|
export { writeUSDA } from './usd/usda/writer.js';
|
|
14
15
|
export { readUSDC } from './usd/usdc/reader.js';
|
|
15
16
|
export { writeUSDC } from './usd/usdc/writer.js';
|
|
17
|
+
export { ConversionLimitError } from './limits.js';
|
|
16
18
|
|
|
17
19
|
function toBytes( data ) {
|
|
18
20
|
|
|
@@ -20,37 +22,50 @@ function toBytes( data ) {
|
|
|
20
22
|
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
// Limits shared by both directions; see README "Limits".
|
|
26
|
+
function budgetFor( { maxDecodedBytes, maxOutputBytes, signal } ) {
|
|
27
|
+
|
|
28
|
+
return new Budget( { maxDecodedBytes, maxOutputBytes, signal } );
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
/**
|
|
24
33
|
* Converts USD (usda, usdc or usdz, detected from the bytes) to GLB.
|
|
25
34
|
* @param {ArrayBuffer|Uint8Array} data
|
|
26
|
-
* @param {{ identifier?: string, resolver?: ( path: string ) => Promise<Uint8Array>, variants?: object, log?: ( message: string ) => void }} [options]
|
|
35
|
+
* @param {{ identifier?: string, resolver?: ( path: string ) => Promise<Uint8Array>, variants?: object, log?: ( message: string ) => void, maxDecodedBytes?: number, maxOutputBytes?: number, signal?: AbortSignal }} [options]
|
|
27
36
|
* resolver loads files referenced by relative paths (not needed for self-contained usdz)
|
|
28
37
|
* @returns {Promise<Uint8Array>}
|
|
29
38
|
*/
|
|
30
39
|
export async function usdToGLB( data, options = {} ) {
|
|
31
40
|
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
41
|
+
const budget = budgetFor( options );
|
|
42
|
+
const stage = await openStage( toBytes( data ), { ...options, budget } );
|
|
43
|
+
const builder = await new USDToGLTF( stage, { ...options, budget } ).convert();
|
|
44
|
+
const glb = builder.toGLB();
|
|
45
|
+
budget.check();
|
|
46
|
+
return glb;
|
|
35
47
|
|
|
36
48
|
}
|
|
37
49
|
|
|
38
50
|
/**
|
|
39
51
|
* Converts GLB (or glTF JSON with embedded or resolvable buffers) to USD.
|
|
40
52
|
* @param {ArrayBuffer|Uint8Array} data
|
|
41
|
-
* @param {{ format?: 'usda'|'usdc'|'usdz', resolver?: ( uri: string ) => Promise<Uint8Array
|
|
53
|
+
* @param {{ format?: 'usda'|'usdc'|'usdz', resolver?: ( uri: string ) => Promise<Uint8Array>, maxDecodedBytes?: number, maxOutputBytes?: number, signal?: AbortSignal }} [options]
|
|
42
54
|
* @returns {Promise<Uint8Array>}
|
|
43
55
|
*/
|
|
44
|
-
export async function glbToUSD( data, { format = 'usdz', resolver = null, log, imageDecoder } = {} ) {
|
|
56
|
+
export async function glbToUSD( data, { format = 'usdz', resolver = null, log, imageDecoder, ...limits } = {} ) {
|
|
45
57
|
|
|
46
|
-
const
|
|
58
|
+
const budget = budgetFor( limits );
|
|
59
|
+
const doc = await readGLTF( toBytes( data ), { resolver, budget } );
|
|
47
60
|
const { layer, files } = await new GLTFToUSD( doc, { log, imageDecoder } ).convert();
|
|
61
|
+
let out;
|
|
48
62
|
|
|
49
|
-
if ( format === 'usda' )
|
|
50
|
-
if ( format === 'usdc' )
|
|
63
|
+
if ( format === 'usda' ) out = new TextEncoder().encode( writeUSDA( layer, { budget } ) );
|
|
64
|
+
else if ( format === 'usdc' ) out = writeUSDC( layer, { budget } );
|
|
65
|
+
else out = writeUSDZ( [ { name: 'root.usdc', data: writeUSDC( layer, { budget } ) }, ...files ], { budget } ); // the root layer must come first
|
|
51
66
|
|
|
52
|
-
|
|
53
|
-
return
|
|
67
|
+
budget.check();
|
|
68
|
+
return out;
|
|
54
69
|
|
|
55
70
|
}
|
|
56
71
|
|
|
@@ -58,10 +73,14 @@ export async function glbToUSD( data, { format = 'usdz', resolver = null, log, i
|
|
|
58
73
|
* For usda / usdc output the textures are separate files; this returns them alongside the layer.
|
|
59
74
|
* @returns {Promise<{ layer: Uint8Array, files: Array<{ name: string, data: Uint8Array }> }>}
|
|
60
75
|
*/
|
|
61
|
-
export async function glbToUSDFiles( data, { format = 'usda', resolver = null, log, imageDecoder } = {} ) {
|
|
76
|
+
export async function glbToUSDFiles( data, { format = 'usda', resolver = null, log, imageDecoder, ...limits } = {} ) {
|
|
62
77
|
|
|
63
|
-
const
|
|
78
|
+
const budget = budgetFor( limits );
|
|
79
|
+
const doc = await readGLTF( toBytes( data ), { resolver, budget } );
|
|
64
80
|
const { layer, files } = await new GLTFToUSD( doc, { log, imageDecoder } ).convert();
|
|
65
|
-
|
|
81
|
+
const bytes = format === 'usdc' ? writeUSDC( layer, { budget } ) : new TextEncoder().encode( writeUSDA( layer, { budget } ) );
|
|
82
|
+
budget.output( files.reduce( ( n, f ) => n + f.data.byteLength, bytes.byteLength ), 'usd files' );
|
|
83
|
+
budget.check();
|
|
84
|
+
return { layer: bytes, files };
|
|
66
85
|
|
|
67
86
|
}
|