import { create } from 'xmlbuilder2'; import { decompressSync } from 'fflate';
import DataAccessHelper from 'vtk.js/Sources/IO/Core/DataAccessHelper'; import Base64 from 'vtk.js/Sources/Common/Core/Base64'; import macro from 'vtk.js/Sources/macros'; import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; import BinaryHelper from 'vtk.js/Sources/IO/Core/BinaryHelper';
import 'vtk.js/Sources/IO/Core/DataAccessHelper/LiteHttpDataAccessHelper';
export function findAllTags(node, tagName) { return [...node.getElementsByTagName(tagName)]; }
export function findFirstTag(node, tagName) { return findAllTags(node, tagName)[0]; }
function parseXML(xmlStr) { return create(xmlStr); }
function extractAppendedData(buffer) { const prefixRegex = /^\s*<AppendedData\s+encoding="raw">\s*_/m; const suffixRegex = /\n\s*<\/AppendedData>/m; return BinaryHelper.extractBinary(buffer, prefixRegex, suffixRegex); }
const TYPED_ARRAY = { Int8: Int8Array, UInt8: Uint8Array, Int16: Int16Array, UInt16: Uint16Array, Int32: Int32Array, UInt32: Uint32Array, Int64: Int32Array, UInt64: Uint32Array, Float32: Float32Array, Float64: Float64Array, };
const TYPED_ARRAY_BYTES = { Int8: 1, UInt8: 1, Int16: 2, UInt16: 2, Int32: 4, UInt32: 4, Int64: 8, UInt64: 8, Float32: 4, Float64: 8, };
function integer64to32(array) { const maxIdx = array.length - 1; return array.filter((v, i) => i < maxIdx && i % 2 === 0); }
function readerHeader(uint8, headerType) { if (headerType === 'UInt64') { const offset = 8; let uint32 = new Uint32Array(uint8.buffer, 0, 6); const nbBlocks = uint32[0]; const s1 = uint32[2]; const s2 = uint32[4]; const resultArray = [offset, nbBlocks, s1, s2]; uint32 = new Uint32Array(uint8.buffer, 3 * 8, nbBlocks * 2); for (let i = 0; i < nbBlocks; i++) { resultArray.push(uint32[i * 2]); } return resultArray; } let uint32 = new Uint32Array(uint8.buffer, 0, 3); const offset = 4; const nbBlocks = uint32[0]; const s1 = uint32[1]; const s2 = uint32[2]; const resultArray = [offset, nbBlocks, s1, s2]; uint32 = new Uint32Array(uint8.buffer, 3 * 4, nbBlocks); for (let i = 0; i < nbBlocks; i++) { resultArray.push(uint32[i]); } return resultArray; }
function uncompressBlock(compressedUint8, output) { const uncompressedBlock = decompressSync(compressedUint8); output.uint8.set(uncompressedBlock, output.offset); output.offset += uncompressedBlock.length; }
function processDataArray( size, dataArrayElem, compressor, byteOrder, headerType, binaryBuffer ) { const dataType = dataArrayElem.getAttribute('type'); const name = dataArrayElem.getAttribute('Name'); const format = dataArrayElem.getAttribute('format'); const numberOfComponents = Number( dataArrayElem.getAttribute('NumberOfComponents') || '1' ); let values = null;
if (format === 'ascii') { values = new TYPED_ARRAY[dataType](size * numberOfComponents); let offset = 0; dataArrayElem.firstChild.nodeValue.split(/[\\t \\n]+/).forEach((token) => { if (token.trim().length) { values[offset++] = Number(token); } }); } else if (format === 'binary') { const uint8 = new Uint8Array( Base64.toArrayBuffer(dataArrayElem.firstChild.nodeValue.trim()) ); if (compressor === 'vtkZLibDataCompressor') { const buffer = new ArrayBuffer( TYPED_ARRAY_BYTES[dataType] * size * numberOfComponents ); values = new TYPED_ARRAY[dataType](buffer); const output = { offset: 0, uint8: new Uint8Array(buffer), }; const header = readerHeader(uint8, headerType); const nbBlocks = header[1]; let offset = uint8.length - (header.reduce((a, b) => a + b, 0) - (header[0] + header[1] + header[2] + header[3])); for (let i = 0; i < nbBlocks; i++) { const blockSize = header[4 + i]; const compressedBlock = new Uint8Array(uint8.buffer, offset, blockSize); uncompressBlock(compressedBlock, output); offset += blockSize; }
if (dataType.indexOf('Int64') !== -1) { values = integer64to32(values); } } else { values = new TYPED_ARRAY[dataType]( uint8.buffer, TYPED_ARRAY_BYTES[headerType] );
if (dataType.indexOf('Int64') !== -1) { values = integer64to32(values); } } } else if (format === 'appended') { let offset = Number(dataArrayElem.getAttribute('offset')); let header; if (offset % TYPED_ARRAY_BYTES[headerType] === 0) { header = new TYPED_ARRAY[headerType](binaryBuffer, offset, 1); } else { header = new TYPED_ARRAY[headerType]( binaryBuffer.slice(offset, offset + TYPED_ARRAY_BYTES[headerType]) ); } let arraySize = header[0] / TYPED_ARRAY_BYTES[dataType];
if (dataType.indexOf('Int64') !== -1) { arraySize *= 2; }
offset += TYPED_ARRAY_BYTES[headerType];
if (offset % TYPED_ARRAY_BYTES[dataType] === 0) { values = new TYPED_ARRAY[dataType](binaryBuffer, offset, arraySize); } else { values = new TYPED_ARRAY[dataType]( binaryBuffer.slice(offset, offset + header[0]) ); } if (dataType.indexOf('Int64') !== -1) { values = integer64to32(values); } } else { console.error('Format not supported', format); }
return { name, values, numberOfComponents }; }
function processCells( size, containerElem, compressor, byteOrder, headerType, binaryBuffer ) { const arrayElems = {}; const dataArrayElems = containerElem.getElementsByTagName('DataArray'); for (let elIdx = 0; elIdx < dataArrayElems.length; elIdx++) { const el = dataArrayElems[elIdx]; arrayElems[el.getAttribute('Name')] = el; }
const offsets = processDataArray( size, arrayElems.offsets, compressor, byteOrder, headerType, binaryBuffer ).values; const connectivitySize = offsets[offsets.length - 1]; const connectivity = processDataArray( connectivitySize, arrayElems.connectivity, compressor, byteOrder, headerType, binaryBuffer ).values; const values = new Uint32Array(size + connectivitySize); let writeOffset = 0; let previousOffset = 0; offsets.forEach((v) => { const cellSize = v - previousOffset; values[writeOffset++] = cellSize;
for (let i = 0; i < cellSize; i++) { values[writeOffset++] = connectivity[previousOffset + i]; }
previousOffset = v; });
return values; }
function processFieldData( size, fieldElem, fieldContainer, compressor, byteOrder, headerType, binaryBuffer ) { if (fieldElem) { const attributes = ['Scalars', 'Vectors', 'Normals', 'Tensors', 'TCoords']; const nameBinding = {}; attributes.forEach((attrName) => { const arrayName = fieldElem.getAttribute(attrName); if (arrayName) { nameBinding[arrayName] = fieldContainer[`set${attrName}`]; } });
const dataArrayElems = fieldElem.getElementsByTagName('DataArray'); const nbArrays = dataArrayElems.length; for (let idx = 0; idx < nbArrays; idx++) { const array = dataArrayElems[idx]; const dataArray = vtkDataArray.newInstance( processDataArray( size, array, compressor, byteOrder, headerType, binaryBuffer ) ); const name = dataArray.getName(); (nameBinding[name] || fieldContainer.addArray)(dataArray); } } }
function handleFieldDataArrays( fieldDataElem, compressor, byteOrder, headerType, binaryBuffer ) { return [...fieldDataElem.getElementsByTagName('DataArray')].map((daElem) => vtkDataArray.newInstance( processDataArray( Number(daElem.getAttribute('NumberOfTuples')), daElem, compressor, byteOrder, headerType, binaryBuffer ) ) ); }
function vtkXMLReader(publicAPI, model) { model.classHierarchy.push('vtkXMLReader');
if (!model.dataAccessHelper) { model.dataAccessHelper = DataAccessHelper.get('http'); }
function fetchData(url, option = {}) { return model.dataAccessHelper.fetchBinary(url, option); }
publicAPI.setUrl = (url, option = {}) => { model.url = url;
const path = url.split('/'); path.pop(); model.baseURL = path.join('/');
return publicAPI.loadData(option); };
publicAPI.loadData = (option = {}) => fetchData(model.url, option).then(publicAPI.parseAsArrayBuffer);
publicAPI.parseAsArrayBuffer = (arrayBuffer) => { if (!arrayBuffer) { return false; } if (arrayBuffer !== model.rawDataBuffer) { publicAPI.modified(); } else { return true; }
const { text: content, binaryBuffer } = extractAppendedData(arrayBuffer); model.rawDataBuffer = arrayBuffer; model.binaryBuffer = binaryBuffer;
const doc = parseXML(content); const rootElem = doc.root().node; const type = rootElem.getAttribute('type'); const compressor = rootElem.getAttribute('compressor'); const byteOrder = rootElem.getAttribute('byte_order'); const 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; }
if (findFirstTag(rootElem, 'AppendedData')) { const appendedDataElem = findFirstTag(rootElem, 'AppendedData'); const encoding = appendedDataElem.getAttribute('encoding'); const arrayElems = findAllTags(rootElem, 'DataArray');
let appendedBuffer = model.binaryBuffer;
if (encoding === 'base64') { appendedBuffer = appendedDataElem.textContent.trim().substr(1); }
const dataArrays = []; for (let i = 0; i < arrayElems.length; ++i) { const offset = Number(arrayElems[i].getAttribute('offset')); let 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 { dataArrays.push( new Uint8Array(appendedBuffer.slice(offset, nextOffset)) ); } }
if (compressor === 'vtkZLibDataCompressor') { for (let arrayidx = 0; arrayidx < dataArrays.length; ++arrayidx) { const dataArray = dataArrays[arrayidx];
const header = readerHeader(dataArray, headerType); const nbBlocks = header[1]; let compressedOffset = dataArray.length - (header.reduce((a, b) => a + b, 0) - (header[0] + header[1] + header[2] + header[3]));
let buffer = null; if (nbBlocks > 0) { if (header[3] === 0) { buffer = new ArrayBuffer(header[2] * nbBlocks); } else { buffer = new ArrayBuffer(header[2] * (nbBlocks - 1) + header[3]); } } else { buffer = new ArrayBuffer(0); }
const uncompressed = new Uint8Array(buffer); const output = { offset: 0, uint8: uncompressed, };
for (let i = 0; i < nbBlocks; i++) { const blockSize = header[4 + i]; const compressedBlock = new Uint8Array( dataArray.buffer, compressedOffset, blockSize ); uncompressBlock(compressedBlock, output); compressedOffset += blockSize; }
const data = new Uint8Array( uncompressed.length + TYPED_ARRAY_BYTES[headerType] ); new TYPED_ARRAY[headerType](data.buffer, 0, 1)[0] = uncompressed.length; data.set(uncompressed, TYPED_ARRAY_BYTES[headerType]);
dataArrays[arrayidx] = data; } }
const bufferLength = dataArrays.reduce((acc, arr) => acc + arr.length, 0); const buffer = new ArrayBuffer(bufferLength); const view = new Uint8Array(buffer);
for (let i = 0, offset = 0; i < dataArrays.length; ++i) { arrayElems[i].setAttribute('offset', offset); view.set(dataArrays[i], offset); offset += dataArrays[i].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);
const datasetElem = rootElem.getElementsByTagName(type)[0]; const fieldDataElem = datasetElem.getElementsByTagName('FieldData')[0];
if (fieldDataElem) { const fieldDataArrays = handleFieldDataArrays( fieldDataElem, compressor, byteOrder, headerType, model.binaryBuffer );
for (let i = 0; i < model.output.length; i++) { const fieldData = model.output[i].getFieldData(); for (let j = 0; j < fieldDataArrays.length; j++) { fieldData.addArray(fieldDataArrays[j]); } } }
return true; };
publicAPI.requestData = (inData, outData) => { publicAPI.parseAsArrayBuffer(model.rawDataBuffer); }; }
const DEFAULT_VALUES = { };
export function extend(publicAPI, model, initialValues = {}) { Object.assign(model, DEFAULT_VALUES, initialValues);
macro.obj(publicAPI, model); macro.get(publicAPI, model, ['url', 'baseURL']); macro.setGet(publicAPI, model, ['dataAccessHelper']); macro.algo(publicAPI, model, 0, 1);
vtkXMLReader(publicAPI, model); }
export default { extend, processDataArray, processFieldData, processCells };
|