UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

521 lines (410 loc) 19 kB
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 };