@kitware/vtk.js
Version:
Visualization Toolkit for the Web
521 lines (410 loc) • 19 kB
JavaScript
import _toConsumableArray from '@babel/runtime/helpers/toConsumableArray';
import { create } from 'xmlbuilder2';
import { decompressSync } from 'fflate';
import DataAccessHelper from '../Core/DataAccessHelper.js';
import Base64 from '../../Common/Core/Base64.js';
import macro from '../../macros.js';
import vtkDataArray from '../../Common/Core/DataArray.js';
import BinaryHelper from '../Core/BinaryHelper.js';
import '../Core/DataAccessHelper/LiteHttpDataAccessHelper.js';
// import 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper'; // HTTP + zip
// import 'vtk.js/Sources/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; // html + base64 + zip
// import 'vtk.js/Sources/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; // zip
// ----------------------------------------------------------------------------
// Global methods
// ----------------------------------------------------------------------------
function findAllTags(node, tagName) {
return _toConsumableArray(node.getElementsByTagName(tagName));
}
function findFirstTag(node, tagName) {
return findAllTags(node, tagName)[0];
}
function parseXML(xmlStr) {
// see xmlbuilder2 docs on the object format
return create(xmlStr);
}
function extractAppendedData(buffer) {
// search for appended data tag
var prefixRegex = /^\s*<AppendedData\s+encoding="raw">\s*_/m;
var suffixRegex = /\n\s*<\/AppendedData>/m;
return BinaryHelper.extractBinary(buffer, prefixRegex, suffixRegex);
} // ----------------------------------------------------------------------------
var TYPED_ARRAY = {
Int8: Int8Array,
UInt8: Uint8Array,
Int16: Int16Array,
UInt16: Uint16Array,
Int32: Int32Array,
UInt32: Uint32Array,
Int64: Int32Array,
// Not supported with JavaScript will cause error in binary
UInt64: Uint32Array,
// Not supported with JavaScript will cause error in binary
Float32: Float32Array,
Float64: Float64Array
}; // ----------------------------------------------------------------------------
var TYPED_ARRAY_BYTES = {
Int8: 1,
UInt8: 1,
Int16: 2,
UInt16: 2,
Int32: 4,
UInt32: 4,
Int64: 8,
// Not supported with JavaScript will cause error in binary
UInt64: 8,
// Not supported with JavaScript will cause error in binary
Float32: 4,
Float64: 8
}; // ----------------------------------------------------------------------------
function integer64to32(array) {
var maxIdx = array.length - 1; // Skip last
return array.filter(function (v, i) {
return i < maxIdx && i % 2 === 0;
});
} // ----------------------------------------------------------------------------
function readerHeader(uint8, headerType) {
// We do not handle endianness or if more than 32 bits are needed to encode the data
if (headerType === 'UInt64') {
var _offset = 8;
var _uint = new Uint32Array(uint8.buffer, 0, 6);
var _nbBlocks = _uint[0];
var _s = _uint[2];
var _s2 = _uint[4];
var _resultArray = [_offset, _nbBlocks, _s, _s2];
_uint = new Uint32Array(uint8.buffer, 3 * 8, _nbBlocks * 2);
for (var i = 0; i < _nbBlocks; i++) {
_resultArray.push(_uint[i * 2]);
}
return _resultArray;
} // UInt32
var uint32 = new Uint32Array(uint8.buffer, 0, 3);
var offset = 4;
var nbBlocks = uint32[0];
var s1 = uint32[1];
var s2 = uint32[2];
var resultArray = [offset, nbBlocks, s1, s2];
uint32 = new Uint32Array(uint8.buffer, 3 * 4, nbBlocks);
for (var _i = 0; _i < nbBlocks; _i++) {
resultArray.push(uint32[_i]);
}
return resultArray;
} // ----------------------------------------------------------------------------
function uncompressBlock(compressedUint8, output) {
var uncompressedBlock = decompressSync(compressedUint8);
output.uint8.set(uncompressedBlock, output.offset);
output.offset += uncompressedBlock.length;
} // ----------------------------------------------------------------------------
function processDataArray(size, dataArrayElem, compressor, byteOrder, headerType, binaryBuffer) {
var dataType = dataArrayElem.getAttribute('type');
var name = dataArrayElem.getAttribute('Name');
var format = dataArrayElem.getAttribute('format'); // binary, ascii, appended
var numberOfComponents = Number(dataArrayElem.getAttribute('NumberOfComponents') || '1');
var values = null;
if (format === 'ascii') {
values = new TYPED_ARRAY[dataType](size * numberOfComponents);
var offset = 0;
dataArrayElem.firstChild.nodeValue.split(/[\\t \\n]+/).forEach(function (token) {
if (token.trim().length) {
values[offset++] = Number(token);
}
});
} else if (format === 'binary') {
var uint8 = new Uint8Array(Base64.toArrayBuffer(dataArrayElem.firstChild.nodeValue.trim()));
if (compressor === 'vtkZLibDataCompressor') {
var buffer = new ArrayBuffer(TYPED_ARRAY_BYTES[dataType] * size * numberOfComponents);
values = new TYPED_ARRAY[dataType](buffer);
var output = {
offset: 0,
uint8: new Uint8Array(buffer)
}; // ----------------------------------------------------------------------
// Layout of the data
// header[N, s1, s1, blockSize1, ..., blockSizeN], [padding???], block[compressedData], ..., block[compressedData]
// [header] N, s1 and s2 are uint 32 or 64 (defined by header_type="UInt64" attribute on the root node)
// [header] s1: uncompress size of each block except the last one
// [header] s2: uncompress size of the last blocks
// [header] blockSize: size of the block in compressed space that represent to bloc to inflate in zlib. (This also give the offset to the next block)
// ----------------------------------------------------------------------
// Header reading
var header = readerHeader(uint8, headerType);
var nbBlocks = header[1];
var _offset2 = uint8.length - (header.reduce(function (a, b) {
return a + b;
}, 0) - (header[0] + header[1] + header[2] + header[3]));
for (var i = 0; i < nbBlocks; i++) {
var blockSize = header[4 + i];
var compressedBlock = new Uint8Array(uint8.buffer, _offset2, blockSize);
uncompressBlock(compressedBlock, output);
_offset2 += blockSize;
} // Handle (u)int64 hoping for no overflow...
if (dataType.indexOf('Int64') !== -1) {
values = integer64to32(values);
}
} else {
values = new TYPED_ARRAY[dataType](uint8.buffer, TYPED_ARRAY_BYTES[headerType]); // Skip the count
// Handle (u)int64 hoping no overflow...
if (dataType.indexOf('Int64') !== -1) {
values = integer64to32(values);
}
}
} else if (format === 'appended') {
var _offset3 = Number(dataArrayElem.getAttribute('offset')); // read header
// NOTE: this will incorrectly read the size if headerType is (U)Int64 and
// the value requires (U)Int64.
var _header;
if (_offset3 % TYPED_ARRAY_BYTES[headerType] === 0) {
_header = new TYPED_ARRAY[headerType](binaryBuffer, _offset3, 1);
} else {
_header = new TYPED_ARRAY[headerType](binaryBuffer.slice(_offset3, _offset3 + TYPED_ARRAY_BYTES[headerType]));
}
var arraySize = _header[0] / TYPED_ARRAY_BYTES[dataType]; // if we are dealing with Uint64, we need to get double the values since
// TYPED_ARRAY[Uint64] is Uint32.
if (dataType.indexOf('Int64') !== -1) {
arraySize *= 2;
}
_offset3 += TYPED_ARRAY_BYTES[headerType]; // read values
// if offset is aligned to dataType, use view. Otherwise, slice due to misalignment.
if (_offset3 % TYPED_ARRAY_BYTES[dataType] === 0) {
values = new TYPED_ARRAY[dataType](binaryBuffer, _offset3, arraySize);
} else {
values = new TYPED_ARRAY[dataType](binaryBuffer.slice(_offset3, _offset3 + _header[0]));
} // remove higher order 32 bits assuming they're not used.
if (dataType.indexOf('Int64') !== -1) {
values = integer64to32(values);
}
} else {
console.error('Format not supported', format);
}
return {
name: name,
values: values,
numberOfComponents: numberOfComponents
};
} // ----------------------------------------------------------------------------
function processCells(size, containerElem, compressor, byteOrder, headerType, binaryBuffer) {
var arrayElems = {};
var dataArrayElems = containerElem.getElementsByTagName('DataArray');
for (var elIdx = 0; elIdx < dataArrayElems.length; elIdx++) {
var el = dataArrayElems[elIdx];
arrayElems[el.getAttribute('Name')] = el;
}
var offsets = processDataArray(size, arrayElems.offsets, compressor, byteOrder, headerType, binaryBuffer).values;
var connectivitySize = offsets[offsets.length - 1];
var connectivity = processDataArray(connectivitySize, arrayElems.connectivity, compressor, byteOrder, headerType, binaryBuffer).values;
var values = new Uint32Array(size + connectivitySize);
var writeOffset = 0;
var previousOffset = 0;
offsets.forEach(function (v) {
var cellSize = v - previousOffset;
values[writeOffset++] = cellSize;
for (var i = 0; i < cellSize; i++) {
values[writeOffset++] = connectivity[previousOffset + i];
} // save previous offset
previousOffset = v;
});
return values;
} // ----------------------------------------------------------------------------
function processFieldData(size, fieldElem, fieldContainer, compressor, byteOrder, headerType, binaryBuffer) {
if (fieldElem) {
var attributes = ['Scalars', 'Vectors', 'Normals', 'Tensors', 'TCoords'];
var nameBinding = {};
attributes.forEach(function (attrName) {
var arrayName = fieldElem.getAttribute(attrName);
if (arrayName) {
nameBinding[arrayName] = fieldContainer["set".concat(attrName)];
}
});
var dataArrayElems = fieldElem.getElementsByTagName('DataArray');
var nbArrays = dataArrayElems.length;
for (var idx = 0; idx < nbArrays; idx++) {
var array = dataArrayElems[idx];
var dataArray = vtkDataArray.newInstance(processDataArray(size, array, compressor, byteOrder, headerType, binaryBuffer));
var name = dataArray.getName();
(nameBinding[name] || fieldContainer.addArray)(dataArray);
}
}
} // ----------------------------------------------------------------------------
function handleFieldDataArrays(fieldDataElem, compressor, byteOrder, headerType, binaryBuffer) {
return _toConsumableArray(fieldDataElem.getElementsByTagName('DataArray')).map(function (daElem) {
return vtkDataArray.newInstance(processDataArray(Number(daElem.getAttribute('NumberOfTuples')), daElem, compressor, byteOrder, headerType, binaryBuffer));
});
} // ----------------------------------------------------------------------------
// vtkXMLReader methods
// ----------------------------------------------------------------------------
function vtkXMLReader(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkXMLReader'); // Create default dataAccessHelper if not available
if (!model.dataAccessHelper) {
model.dataAccessHelper = DataAccessHelper.get('http');
} // Internal method to fetch Array
function fetchData(url) {
var option = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
return model.dataAccessHelper.fetchBinary(url, option);
} // Set DataSet url
publicAPI.setUrl = function (url) {
var option = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
model.url = url; // Remove the file in the URL
var path = url.split('/');
path.pop();
model.baseURL = path.join('/'); // Fetch metadata
return publicAPI.loadData(option);
}; // Fetch the actual data arrays
publicAPI.loadData = function () {
var option = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return fetchData(model.url, option).then(publicAPI.parseAsArrayBuffer);
};
publicAPI.parseAsArrayBuffer = function (arrayBuffer) {
if (!arrayBuffer) {
return false;
}
if (arrayBuffer !== model.rawDataBuffer) {
publicAPI.modified();
} else {
return true;
}
var _extractAppendedData = extractAppendedData(arrayBuffer),
content = _extractAppendedData.text,
binaryBuffer = _extractAppendedData.binaryBuffer;
model.rawDataBuffer = arrayBuffer;
model.binaryBuffer = binaryBuffer; // Parse data here...
var doc = parseXML(content);
var rootElem = doc.root().node;
var type = rootElem.getAttribute('type');
var compressor = rootElem.getAttribute('compressor');
var byteOrder = rootElem.getAttribute('byte_order'); // default to UInt32. I think version 0.1 vtp/vti files default to UInt32.
var headerType = rootElem.getAttribute('header_type') || 'UInt32';
if (compressor && compressor !== 'vtkZLibDataCompressor') {
console.error('Invalid compressor', compressor);
return false;
}
if (byteOrder && byteOrder !== 'LittleEndian') {
console.error('Only LittleEndian encoding is supported');
return false;
}
if (type !== model.dataType) {
console.error('Invalid data type', type, 'expecting', model.dataType);
return false;
} // appended format
if (findFirstTag(rootElem, 'AppendedData')) {
var appendedDataElem = findFirstTag(rootElem, 'AppendedData');
var encoding = appendedDataElem.getAttribute('encoding');
var arrayElems = findAllTags(rootElem, 'DataArray');
var appendedBuffer = model.binaryBuffer;
if (encoding === 'base64') {
// substr(1) is to remove the '_' prefix
appendedBuffer = appendedDataElem.textContent.trim().substr(1);
} // get data array chunks
var dataArrays = [];
for (var i = 0; i < arrayElems.length; ++i) {
var offset = Number(arrayElems[i].getAttribute('offset'));
var nextOffset = 0;
if (i === arrayElems.length - 1) {
nextOffset = appendedBuffer.length || appendedBuffer.byteLength;
} else {
nextOffset = Number(arrayElems[i + 1].getAttribute('offset'));
}
if (encoding === 'base64') {
dataArrays.push(new Uint8Array(Base64.toArrayBuffer(appendedBuffer.substring(offset, nextOffset))));
} else {
// encoding === 'raw'
// Need to slice the ArrayBuffer so readerHeader() works properly
dataArrays.push(new Uint8Array(appendedBuffer.slice(offset, nextOffset)));
}
}
if (compressor === 'vtkZLibDataCompressor') {
for (var arrayidx = 0; arrayidx < dataArrays.length; ++arrayidx) {
var dataArray = dataArrays[arrayidx]; // Header reading
// Refer to processDataArray() above for info on header fields
var header = readerHeader(dataArray, headerType);
var nbBlocks = header[1];
var compressedOffset = dataArray.length - (header.reduce(function (a, b) {
return a + b;
}, 0) - (header[0] + header[1] + header[2] + header[3]));
var _buffer = null;
if (nbBlocks > 0) {
// If the last block's size is labeled as 0, that means the last block
// really has size header[2].
if (header[3] === 0) {
_buffer = new ArrayBuffer(header[2] * nbBlocks);
} else {
_buffer = new ArrayBuffer(header[2] * (nbBlocks - 1) + header[3]);
}
} else {
// if there is no blocks, then default to a zero array of size 0.
_buffer = new ArrayBuffer(0);
} // uncompressed buffer
var uncompressed = new Uint8Array(_buffer);
var output = {
offset: 0,
uint8: uncompressed
};
for (var _i2 = 0; _i2 < nbBlocks; _i2++) {
var blockSize = header[4 + _i2];
var compressedBlock = new Uint8Array(dataArray.buffer, compressedOffset, blockSize);
uncompressBlock(compressedBlock, output);
compressedOffset += blockSize;
}
var data = new Uint8Array(uncompressed.length + TYPED_ARRAY_BYTES[headerType]); // set length header
// TODO this does not work for lengths that are greater than the max Uint32 value.
new TYPED_ARRAY[headerType](data.buffer, 0, 1)[0] = uncompressed.length;
data.set(uncompressed, TYPED_ARRAY_BYTES[headerType]);
dataArrays[arrayidx] = data;
}
}
var bufferLength = dataArrays.reduce(function (acc, arr) {
return acc + arr.length;
}, 0);
var buffer = new ArrayBuffer(bufferLength);
var view = new Uint8Array(buffer);
for (var _i3 = 0, _offset4 = 0; _i3 < dataArrays.length; ++_i3) {
// set correct offsets
arrayElems[_i3].setAttribute('offset', _offset4); // set final buffer data
view.set(dataArrays[_i3], _offset4);
_offset4 += dataArrays[_i3].length;
}
model.binaryBuffer = buffer;
if (!model.binaryBuffer) {
console.error('Processing appended data format: requires binaryBuffer to parse');
return false;
}
}
publicAPI.parseXML(rootElem, type, compressor, byteOrder, headerType);
var datasetElem = rootElem.getElementsByTagName(type)[0];
var fieldDataElem = datasetElem.getElementsByTagName('FieldData')[0];
if (fieldDataElem) {
var fieldDataArrays = handleFieldDataArrays(fieldDataElem, compressor, byteOrder, headerType, model.binaryBuffer);
for (var _i4 = 0; _i4 < model.output.length; _i4++) {
var fieldData = model.output[_i4].getFieldData();
for (var j = 0; j < fieldDataArrays.length; j++) {
fieldData.addArray(fieldDataArrays[j]);
}
}
}
return true;
};
publicAPI.requestData = function (inData, outData) {
publicAPI.parseAsArrayBuffer(model.rawDataBuffer);
};
} // ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
var DEFAULT_VALUES = {// baseURL: null,
// dataAccessHelper: null,
// url: null,
}; // ----------------------------------------------------------------------------
function extend(publicAPI, model) {
var initialValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
Object.assign(model, DEFAULT_VALUES, initialValues); // Build VTK API
macro.obj(publicAPI, model);
macro.get(publicAPI, model, ['url', 'baseURL']);
macro.setGet(publicAPI, model, ['dataAccessHelper']);
macro.algo(publicAPI, model, 0, 1); // vtkXMLReader methods
vtkXMLReader(publicAPI, model);
} // ----------------------------------------------------------------------------
var vtkXMLReader$1 = {
extend: extend,
processDataArray: processDataArray,
processFieldData: processFieldData,
processCells: processCells
};
export { vtkXMLReader$1 as default, extend, findAllTags, findFirstTag };