Skip to content

Commit

Permalink
GLB Parser start on arbitrary byteOffset (#163)
Browse files Browse the repository at this point in the history
  • Loading branch information
ibgreen committed Apr 13, 2019
1 parent 7e3fec1 commit 1f29f5c
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 90 deletions.
118 changes: 28 additions & 90 deletions modules/gltf/src/glb/glb-parser.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/* eslint-disable camelcase, max-statements */
import {assert} from '@loaders.gl/core';

import {parseGLBSync} from './parse-glb';
import unpackGLBBuffers from './unpack-glb-buffers';
import unpackBinaryJson from '../packed-json/unpack-binary-json';

import {TextDecoder, padTo4Bytes, assert} from '@loaders.gl/core';
import {
ATTRIBUTE_TYPE_TO_COMPONENTS,
ATTRIBUTE_COMPONENT_TYPE_TO_BYTE_SIZE,
Expand All @@ -11,54 +13,49 @@ import {

const MAGIC_glTF = 0x676c5446; // glTF in Big-Endian ASCII

const GLB_FILE_HEADER_SIZE = 12;
const GLB_CHUNK_HEADER_SIZE = 8;

const GLB_CHUNK_TYPE_JSON = 0x4e4f534a;
const GLB_CHUNK_TYPE_BIN = 0x004e4942;

const LE = true; // Binary GLTF is little endian.
const BE = false; // Magic needs to be written as BE

function getMagicString(dataView) {
return `\
${String.fromCharCode(dataView.getUint8(0))}\
${String.fromCharCode(dataView.getUint8(1))}\
${String.fromCharCode(dataView.getUint8(2))}\
${String.fromCharCode(dataView.getUint8(3))}`;
}

// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#glb-file-format-specification
export default class GLBParser {
static isGLB(arrayBuffer, options = {}) {
// Check that GLB Header starts with the magic number
const {magic = MAGIC_glTF} = options;
const dataView = new DataView(arrayBuffer);
const magic1 = dataView.getUint32(0, BE);
const magic1 = dataView.getUint32(0, false);
return magic1 === magic || magic1 === MAGIC_glTF;
}

constructor(options = {}) {
// Result
this.binaryByteOffset = null;
this.packedJson = null;
this.json = null;
// Return the gltf JSON and the original arrayBuffer
parse(arrayBuffer, options = {}) {
return this.parseSync(arrayBuffer, options);
}

parseSync(arrayBuffer, options = {}) {
// Input
this.glbArrayBuffer = arrayBuffer;

this.binaryByteOffset = null;
this.packedJson = null;
this.json = null;

// Only parse once
if (this.json === null && this.binaryByteOffset === null) {
this.result = this._parse(options);
const byteOffset = 0;

// Populates the supplied object (`this`) with parsed data members.
parseGLBSync(this, this.glbArrayBuffer, byteOffset, options);

// Backwards compat
this.binaryByteOffset = this.binChunkByteOffset;

// Unpack binary JSON
this.packedJson = this.json;
this.unpackedBuffers = unpackGLBBuffers(
this.glbArrayBuffer,
this.json,
this.binaryByteOffset
);
this.json = unpackBinaryJson(this.json, this.unpackedBuffers);
}
return this;
}

// Return the gltf JSON and the original arrayBuffer
parse(arrayBuffer, options = {}) {
return this.parseSync(arrayBuffer, options);
return this;
}

// Returns application JSON data stored in `key`
Expand Down Expand Up @@ -137,63 +134,4 @@ export default class GLBParser {
img.src = imageUrl;
});
}

// PRIVATE

_parse(options) {
const result = this._parseBinary(options);
this.packedJson = result.json;
this.unpackedBuffers = unpackGLBBuffers(this.glbArrayBuffer, this.json, this.binaryByteOffset);
this.json = unpackBinaryJson(this.json, this.unpackedBuffers);
}

_parseBinary(options) {
const {magic = MAGIC_glTF} = options;

// GLB Header
const dataView = new DataView(this.glbArrayBuffer);
const magic1 = dataView.getUint32(0, BE); // Magic number (the ASCII string 'glTF').
const version = dataView.getUint32(4, LE); // Version 2 of binary glTF container format
const fileLength = dataView.getUint32(8, LE); // Total byte length of generated file

let valid = magic1 === MAGIC_glTF || magic1 === magic;
if (!valid) {
console.warn(`Invalid GLB magic string ${getMagicString(dataView)}`); // eslint-disable-line
}

assert(version === 2, `Invalid GLB version ${version}. Only .glb v2 supported`);
assert(fileLength > 20);

// Write the JSON chunk
const jsonChunkLength = dataView.getUint32(12, LE); // Byte length of json chunk
const jsonChunkFormat = dataView.getUint32(16, LE); // Chunk format as uint32

valid = jsonChunkFormat === GLB_CHUNK_TYPE_JSON || jsonChunkFormat === 0; // Back compat
assert(valid, `JSON chunk format ${jsonChunkFormat}`);

// Create a "view" of the binary encoded JSON data
const jsonChunkOffset = GLB_FILE_HEADER_SIZE + GLB_CHUNK_HEADER_SIZE; // First headers: 20 bytes
const jsonChunk = new Uint8Array(this.glbArrayBuffer, jsonChunkOffset, jsonChunkLength);

// Decode the JSON binary array into clear text
const textDecoder = new TextDecoder('utf8');
const jsonText = textDecoder.decode(jsonChunk);

// Parse the JSON text into a JavaScript data structure
this.json = JSON.parse(jsonText);

// TODO - BIN chunk can be optional
const binaryChunkStart = jsonChunkOffset + padTo4Bytes(jsonChunkLength);
this.binaryByteOffset = binaryChunkStart + GLB_CHUNK_HEADER_SIZE;

const binChunkFormat = dataView.getUint32(binaryChunkStart + 4, LE); // Chunk format as uint32
valid = binChunkFormat === GLB_CHUNK_TYPE_BIN || binChunkFormat === 1; // Back compat
assert(valid, `BIN chunk format ${binChunkFormat}`);

return {
arrayBuffer: this.glbArrayBuffer,
binaryByteOffset: this.binaryByteOffset,
json: this.json
};
}
}
109 changes: 109 additions & 0 deletions modules/gltf/src/glb/parse-glb.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/* eslint-disable camelcase, max-statements */
import {TextDecoder, padTo4Bytes, assert} from '@loaders.gl/core';

const MAGIC_glTF = 0x676c5446; // glTF in Big-Endian ASCII

const GLB_FILE_HEADER_SIZE = 12;
const GLB_CHUNK_HEADER_SIZE = 8;

const GLB_CHUNK_TYPE_JSON = 0x4e4f534a;
const GLB_CHUNK_TYPE_BIN = 0x004e4942;

const LE = true; // Binary GLTF is little endian.
const BE = false; // Magic needs to be written as BE

function getMagicString(dataView) {
return `\
${String.fromCharCode(dataView.getUint8(0))}\
${String.fromCharCode(dataView.getUint8(1))}\
${String.fromCharCode(dataView.getUint8(2))}\
${String.fromCharCode(dataView.getUint8(3))}`;
}

// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#glb-file-format-specification
/*
Returns {
// Header
type: String,
magic: number,
version: number,
byteLength: number,
byteOffset: number,
// JSON Chunk
json: any,
jsonChunkFormat: number,
jsonChunkByteOffset: number,
jsonChunkLength: number,
// BIN Chunk
hasBinChunk: boolean,
binChunkFormat: number,
binChunkByteOffset: number,
binChunkLength: number
}
*/
export function parseGLBSync(glb, arrayBuffer, byteOffset = 0, options = {}) {
// Check that GLB Header starts with the magic number
const dataView = new DataView(arrayBuffer);

glb.byteOffset = byteOffset; // Byte offset into the initial arrayBuffer

// GLB Header
glb.magic = dataView.getUint32(byteOffset + 0, BE); // Magic number (the ASCII string 'glTF').
glb.version = dataView.getUint32(byteOffset + 4, LE); // Version 2 of binary glTF container format
glb.byteLength = dataView.getUint32(byteOffset + 8, LE); // Total byte length of generated file

glb.type = getMagicString(dataView);

// TODO - switch type checks to use strings
const {magic = MAGIC_glTF} = options;
const isMagicValid = glb.magic === MAGIC_glTF || glb.magic === magic;
if (!isMagicValid) {
console.warn(`Invalid GLB magic string ${glb.type}`); // eslint-disable-line
}

assert(glb.version === 2, `Invalid GLB version ${glb.version}. Only .glb v2 supported`);
assert(glb.byteLength > 20);

// Parse the JSON chunk

glb.jsonChunkLength = dataView.getUint32(byteOffset + 12, LE); // Byte length of json chunk
glb.jsonChunkFormat = dataView.getUint32(byteOffset + 16, LE); // Chunk format as uint32

// Check JSON Chunk format (0 = Back compat)
const isJSONChunk = glb.jsonChunkFormat === GLB_CHUNK_TYPE_JSON || glb.jsonChunkFormat === 0;
assert(isJSONChunk, `JSON chunk format ${glb.jsonChunkFormat}`);

// Create a "view" of the binary encoded JSON data
glb.jsonChunkByteOffset = GLB_FILE_HEADER_SIZE + GLB_CHUNK_HEADER_SIZE; // First headers: 20 bytes
const jsonChunk = new Uint8Array(
arrayBuffer,
byteOffset + glb.jsonChunkByteOffset,
glb.jsonChunkLength
);

// Decode the JSON binary array into clear text
const textDecoder = new TextDecoder('utf8');
const jsonText = textDecoder.decode(jsonChunk);

// Parse the JSON text into a JavaScript data structure
glb.json = JSON.parse(jsonText);

const binChunkStart = glb.jsonChunkByteOffset + padTo4Bytes(glb.jsonChunkLength);

// Parse and check BIN chunk header
// Note: BIN chunk can be optional
glb.hasBinChunk = binChunkStart + 8 <= glb.byteLength;
glb.binChunkByteOffset = 0;
glb.binChunkLength = 0;

if (glb.hasBinChunk) {
glb.binChunkLength = dataView.getUint32(byteOffset + binChunkStart + 0, LE);
glb.binChunkFormat = dataView.getUint32(byteOffset + binChunkStart + 4, LE);
const isBinChunk = glb.binChunkFormat === GLB_CHUNK_TYPE_BIN || glb.binChunkFormat === 1; // Back compat
assert(isBinChunk, `BIN chunk format ${glb.binChunkFormat}`);

glb.binChunkByteOffset = binChunkStart + GLB_CHUNK_HEADER_SIZE;
}

return byteOffset + glb.byteLength;
}
2 changes: 2 additions & 0 deletions modules/gltf/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ export {default as GLBParser} from './glb/glb-parser';
export {default as GLBBuilder} from './glb/glb-builder';

export {KHR_DRACO_MESH_COMPRESSION, UBER_POINT_CLOUD_EXTENSION} from './gltf/gltf-constants';

export {parseGLBSync as _parseGLBSync} from './glb/parse-glb';

0 comments on commit 1f29f5c

Please sign in to comment.