import { BufferReader } from './buffer-reader.js'; import { KTX2_ID } from './constants-internal.js'; import { KHR_DF_SAMPLE_DATATYPE_SIGNED, type Supercompression, type VKFormat } from './constants.js'; import type { KTX2BasicFormatSample, KTX2Container, KTX2DataFormatDescriptorBasicFormat } from './container.js'; import { createDefaultContainer } from './create-default-container.js'; import { decodeText } from './util.js'; /** * Parses a KTX 2.0 file, returning an unpacked {@link KTX2Container} instance with all associated * data. The container's mip levels and other binary data are pointers into the original file, not * copies, so the original file should not be overwritten after reading. * * @param data Bytes of KTX 2.0 file, as Uint8Array or Buffer. */ export function read(data: Uint8Array): KTX2Container { /////////////////////////////////////////////////// // KTX 2.0 Identifier. /////////////////////////////////////////////////// const id = new Uint8Array(data.buffer, data.byteOffset, KTX2_ID.length); if ( id[0] !== KTX2_ID[0] || // '´' id[1] !== KTX2_ID[1] || // 'K' id[2] !== KTX2_ID[2] || // 'T' id[3] !== KTX2_ID[3] || // 'X' id[4] !== KTX2_ID[4] || // ' ' id[5] !== KTX2_ID[5] || // '2' id[6] !== KTX2_ID[6] || // '0' id[7] !== KTX2_ID[7] || // 'ª' id[8] !== KTX2_ID[8] || // '\r' id[9] !== KTX2_ID[9] || // '\n' id[10] !== KTX2_ID[10] || // '\x1A' id[11] !== KTX2_ID[11] // '\n' ) { throw new Error('Missing KTX 2.0 identifier.'); } const container = createDefaultContainer(); /////////////////////////////////////////////////// // Header. /////////////////////////////////////////////////// const headerByteLength = 17 * Uint32Array.BYTES_PER_ELEMENT; const headerReader = new BufferReader(data, KTX2_ID.length, headerByteLength, true); container.vkFormat = headerReader._nextUint32() as VKFormat; container.typeSize = headerReader._nextUint32(); container.pixelWidth = headerReader._nextUint32(); container.pixelHeight = headerReader._nextUint32(); container.pixelDepth = headerReader._nextUint32(); container.layerCount = headerReader._nextUint32(); container.faceCount = headerReader._nextUint32(); container.levelCount = headerReader._nextUint32(); container.supercompressionScheme = headerReader._nextUint32() as Supercompression; const dfdByteOffset = headerReader._nextUint32(); const dfdByteLength = headerReader._nextUint32(); const kvdByteOffset = headerReader._nextUint32(); const kvdByteLength = headerReader._nextUint32(); const sgdByteOffset = headerReader._nextUint64(); const sgdByteLength = headerReader._nextUint64(); /////////////////////////////////////////////////// // Level Index. /////////////////////////////////////////////////// const levelByteLength = Math.max(container.levelCount, 1) * 3 * 8; const levelReader = new BufferReader(data, KTX2_ID.length + headerByteLength, levelByteLength, true); for (let i = 0, il = Math.max(container.levelCount, 1); i < il; i++) { container.levels.push({ levelData: new Uint8Array(data.buffer, data.byteOffset + levelReader._nextUint64(), levelReader._nextUint64()), uncompressedByteLength: levelReader._nextUint64(), }); } /////////////////////////////////////////////////// // Data Format Descriptor (DFD). /////////////////////////////////////////////////// const dfdReader = new BufferReader(data, dfdByteOffset, dfdByteLength, true); dfdReader._skip(4); // totalSize const vendorId = dfdReader._nextUint16(); const descriptorType = dfdReader._nextUint16(); const versionNumber = dfdReader._nextUint16(); const descriptorBlockSize = dfdReader._nextUint16(); const colorModel = dfdReader._nextUint8(); const colorPrimaries = dfdReader._nextUint8(); const transferFunction = dfdReader._nextUint8(); const flags = dfdReader._nextUint8(); const texelBlockDimension = [ dfdReader._nextUint8(), dfdReader._nextUint8(), dfdReader._nextUint8(), dfdReader._nextUint8(), ] as KTX2DataFormatDescriptorBasicFormat['texelBlockDimension']; const bytesPlane = [ dfdReader._nextUint8(), dfdReader._nextUint8(), dfdReader._nextUint8(), dfdReader._nextUint8(), dfdReader._nextUint8(), dfdReader._nextUint8(), dfdReader._nextUint8(), dfdReader._nextUint8(), ] as KTX2DataFormatDescriptorBasicFormat['bytesPlane']; const samples = [] as KTX2BasicFormatSample[]; const dfd: KTX2DataFormatDescriptorBasicFormat = { vendorId, descriptorType, versionNumber, colorModel, colorPrimaries, transferFunction, flags, texelBlockDimension, bytesPlane, samples, }; const sampleStart = 6; const sampleWords = 4; const numSamples = (descriptorBlockSize / 4 - sampleStart) / sampleWords; for (let i = 0; i < numSamples; i++) { const sample = { bitOffset: dfdReader._nextUint16(), bitLength: dfdReader._nextUint8(), channelType: dfdReader._nextUint8(), samplePosition: [dfdReader._nextUint8(), dfdReader._nextUint8(), dfdReader._nextUint8(), dfdReader._nextUint8()], sampleLower: Number.NEGATIVE_INFINITY, sampleUpper: Number.POSITIVE_INFINITY, }; if (sample.channelType & KHR_DF_SAMPLE_DATATYPE_SIGNED) { sample.sampleLower = dfdReader._nextInt32(); sample.sampleUpper = dfdReader._nextInt32(); } else { sample.sampleLower = dfdReader._nextUint32(); sample.sampleUpper = dfdReader._nextUint32(); } dfd.samples[i] = sample; } container.dataFormatDescriptor.length = 0; container.dataFormatDescriptor.push(dfd); /////////////////////////////////////////////////// // Key/Value Data (KVD). /////////////////////////////////////////////////// const kvdReader = new BufferReader(data, kvdByteOffset, kvdByteLength, true); while (kvdReader._offset < kvdByteLength) { const keyValueByteLength = kvdReader._nextUint32(); const keyData = kvdReader._scan(keyValueByteLength); const key = decodeText(keyData); container.keyValue[key] = kvdReader._nextUint8Array(keyValueByteLength - keyData.byteLength - 1); if (key.match(/^ktx/i)) { const text = decodeText(container.keyValue[key] as Uint8Array); container.keyValue[key] = text.substring(0, text.lastIndexOf('\x00')); } const kvPadding = keyValueByteLength % 4 ? 4 - (keyValueByteLength % 4) : 0; // align(4) // 4-byte alignment. kvdReader._skip(kvPadding); } /////////////////////////////////////////////////// // Supercompression Global Data (SGD). /////////////////////////////////////////////////// if (sgdByteLength <= 0) return container; const sgdReader = new BufferReader(data, sgdByteOffset, sgdByteLength, true); const endpointCount = sgdReader._nextUint16(); const selectorCount = sgdReader._nextUint16(); const endpointsByteLength = sgdReader._nextUint32(); const selectorsByteLength = sgdReader._nextUint32(); const tablesByteLength = sgdReader._nextUint32(); const extendedByteLength = sgdReader._nextUint32(); const imageDescs = []; for (let i = 0, il = Math.max(container.levelCount, 1); i < il; i++) { imageDescs.push({ imageFlags: sgdReader._nextUint32(), rgbSliceByteOffset: sgdReader._nextUint32(), rgbSliceByteLength: sgdReader._nextUint32(), alphaSliceByteOffset: sgdReader._nextUint32(), alphaSliceByteLength: sgdReader._nextUint32(), }); } const endpointsByteOffset = sgdByteOffset + sgdReader._offset; const selectorsByteOffset = endpointsByteOffset + endpointsByteLength; const tablesByteOffset = selectorsByteOffset + selectorsByteLength; const extendedByteOffset = tablesByteOffset + tablesByteLength; const endpointsData = new Uint8Array(data.buffer, data.byteOffset + endpointsByteOffset, endpointsByteLength); const selectorsData = new Uint8Array(data.buffer, data.byteOffset + selectorsByteOffset, selectorsByteLength); const tablesData = new Uint8Array(data.buffer, data.byteOffset + tablesByteOffset, tablesByteLength); const extendedData = new Uint8Array(data.buffer, data.byteOffset + extendedByteOffset, extendedByteLength); container.globalData = { endpointCount, selectorCount, imageDescs, endpointsData, selectorsData, tablesData, extendedData, }; return container; }