| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- import { HEADER_BYTE_LENGTH, KTX2_ID, KTX_WRITER, NUL } from './constants-internal.js';
- import {
- KHR_DF_KHR_DESCRIPTORTYPE_BASICFORMAT,
- KHR_DF_SAMPLE_DATATYPE_SIGNED,
- KHR_SUPERCOMPRESSION_NONE,
- } from './constants.js';
- import type { KTX2Container } from './container.js';
- import { concat, encodeText, getBlockByteLength, getPadding, leastCommonMultiple } from './util.js';
-
- interface WriteOptions {
- keepWriter?: boolean;
- }
- const DEFAULT_OPTIONS: WriteOptions = { keepWriter: false };
-
- /**
- * Serializes a {@link KTX2Container} instance to a KTX 2.0 file. Mip levels and other binary data
- * are copied into the resulting Uint8Array, so the original container can safely be edited or
- * destroyed after it is serialized.
- *
- * Options:
- * - keepWriter: If true, 'KTXWriter' key/value field is written as provided by the container.
- * Otherwise, a string for the current ktx-parse version is generated. Default: false.
- *
- * @param container
- * @param options
- */
- export function write(container: KTX2Container, options: WriteOptions = {}): Uint8Array {
- // biome-ignore lint/style/noParameterAssign: Merging defaults only.
- options = { ...DEFAULT_OPTIONS, ...options };
-
- ///////////////////////////////////////////////////
- // Supercompression Global Data (SGD).
- ///////////////////////////////////////////////////
-
- let sgdBuffer = new ArrayBuffer(0);
- if (container.globalData) {
- const sgdHeaderBuffer = new ArrayBuffer(20 + container.globalData.imageDescs.length * 5 * 4);
- const sgdHeaderView = new DataView(sgdHeaderBuffer);
- sgdHeaderView.setUint16(0, container.globalData.endpointCount, true);
- sgdHeaderView.setUint16(2, container.globalData.selectorCount, true);
- sgdHeaderView.setUint32(4, container.globalData.endpointsData.byteLength, true);
- sgdHeaderView.setUint32(8, container.globalData.selectorsData.byteLength, true);
- sgdHeaderView.setUint32(12, container.globalData.tablesData.byteLength, true);
- sgdHeaderView.setUint32(16, container.globalData.extendedData.byteLength, true);
-
- for (let i = 0; i < container.globalData.imageDescs.length; i++) {
- const imageDesc = container.globalData.imageDescs[i];
- sgdHeaderView.setUint32(20 + i * 5 * 4 + 0, imageDesc.imageFlags, true);
- sgdHeaderView.setUint32(20 + i * 5 * 4 + 4, imageDesc.rgbSliceByteOffset, true);
- sgdHeaderView.setUint32(20 + i * 5 * 4 + 8, imageDesc.rgbSliceByteLength, true);
- sgdHeaderView.setUint32(20 + i * 5 * 4 + 12, imageDesc.alphaSliceByteOffset, true);
- sgdHeaderView.setUint32(20 + i * 5 * 4 + 16, imageDesc.alphaSliceByteLength, true);
- }
-
- sgdBuffer = concat([
- sgdHeaderBuffer,
- container.globalData.endpointsData,
- container.globalData.selectorsData,
- container.globalData.tablesData,
- container.globalData.extendedData,
- ]);
- }
-
- ///////////////////////////////////////////////////
- // Key/Value Data (KVD).
- ///////////////////////////////////////////////////
-
- const keyValueData: Uint8Array[] = [];
- const keyValueList = Object.entries({
- ...container.keyValue,
- ...(!options.keepWriter && { KTXwriter: KTX_WRITER }),
- });
-
- keyValueList.sort((a, b) => (a[0] > b[0] ? 1 : -1));
-
- for (const [key, value] of keyValueList) {
- const keyData = encodeText(key);
- const valueData = typeof value === 'string' ? concat([encodeText(value), NUL]) : value;
- const kvByteLength = keyData.byteLength + 1 + valueData.byteLength;
- const kvPadding = getPadding(kvByteLength, 4); // align(4)
- keyValueData.push(
- concat([
- new Uint32Array([kvByteLength]),
- keyData,
- NUL,
- valueData,
- new Uint8Array(kvPadding).fill(0x00), // align(4)
- ]),
- );
- }
-
- const kvdBuffer = concat(keyValueData);
-
- ///////////////////////////////////////////////////
- // Data Format Descriptor (DFD).
- ///////////////////////////////////////////////////
-
- if (
- container.dataFormatDescriptor.length !== 1 ||
- container.dataFormatDescriptor[0].descriptorType !== KHR_DF_KHR_DESCRIPTORTYPE_BASICFORMAT
- ) {
- throw new Error('Only BASICFORMAT Data Format Descriptor output supported.');
- }
-
- const dfd = container.dataFormatDescriptor[0];
-
- const dfdBuffer = new ArrayBuffer(28 + dfd.samples.length * 16);
- const dfdView = new DataView(dfdBuffer);
- const descriptorBlockSize = 24 + dfd.samples.length * 16;
-
- dfdView.setUint32(0, dfdBuffer.byteLength, true);
- dfdView.setUint16(4, dfd.vendorId, true);
- dfdView.setUint16(6, dfd.descriptorType, true);
- dfdView.setUint16(8, dfd.versionNumber, true);
- dfdView.setUint16(10, descriptorBlockSize, true);
-
- dfdView.setUint8(12, dfd.colorModel);
- dfdView.setUint8(13, dfd.colorPrimaries);
- dfdView.setUint8(14, dfd.transferFunction);
- dfdView.setUint8(15, dfd.flags);
-
- if (!Array.isArray(dfd.texelBlockDimension)) {
- throw new Error('texelBlockDimension is now an array. For dimensionality `d`, set `d - 1`.');
- }
-
- dfdView.setUint8(16, dfd.texelBlockDimension[0]);
- dfdView.setUint8(17, dfd.texelBlockDimension[1]);
- dfdView.setUint8(18, dfd.texelBlockDimension[2]);
- dfdView.setUint8(19, dfd.texelBlockDimension[3]);
-
- for (let i = 0; i < 8; i++) dfdView.setUint8(20 + i, dfd.bytesPlane[i]);
-
- for (let i = 0; i < dfd.samples.length; i++) {
- const sample = dfd.samples[i];
- const sampleByteOffset = 28 + i * 16;
-
- dfdView.setUint16(sampleByteOffset + 0, sample.bitOffset, true);
- dfdView.setUint8(sampleByteOffset + 2, sample.bitLength);
- dfdView.setUint8(sampleByteOffset + 3, sample.channelType);
-
- dfdView.setUint8(sampleByteOffset + 4, sample.samplePosition[0]);
- dfdView.setUint8(sampleByteOffset + 5, sample.samplePosition[1]);
- dfdView.setUint8(sampleByteOffset + 6, sample.samplePosition[2]);
- dfdView.setUint8(sampleByteOffset + 7, sample.samplePosition[3]);
-
- if (sample.channelType & KHR_DF_SAMPLE_DATATYPE_SIGNED) {
- dfdView.setInt32(sampleByteOffset + 8, sample.sampleLower, true);
- dfdView.setInt32(sampleByteOffset + 12, sample.sampleUpper, true);
- } else {
- dfdView.setUint32(sampleByteOffset + 8, sample.sampleLower, true);
- dfdView.setUint32(sampleByteOffset + 12, sample.sampleUpper, true);
- }
- }
-
- ///////////////////////////////////////////////////
- // Data alignment.
- ///////////////////////////////////////////////////
-
- const dfdByteOffset = KTX2_ID.length + HEADER_BYTE_LENGTH + container.levels.length * 3 * 8;
- const kvdByteOffset = dfdByteOffset + dfdBuffer.byteLength;
- let sgdByteOffset = sgdBuffer.byteLength > 0 ? kvdByteOffset + kvdBuffer.byteLength : 0;
- if (sgdByteOffset % 8) sgdByteOffset += 8 - (sgdByteOffset % 8); // align(8)
-
- ///////////////////////////////////////////////////
- // Level Index.
- ///////////////////////////////////////////////////
-
- const levelData: Uint8Array[] = [];
- const levelIndex = new DataView(new ArrayBuffer(container.levels.length * 3 * 8));
- const levelDataByteOffsets = new Uint32Array(container.levels.length);
-
- let levelAlign = 0;
- if (container.supercompressionScheme === KHR_SUPERCOMPRESSION_NONE) {
- levelAlign = leastCommonMultiple(getBlockByteLength(container), 4);
- }
-
- // Level data is ordered small → large.
- let levelDataByteOffset = (sgdByteOffset || kvdByteOffset + kvdBuffer.byteLength) + sgdBuffer.byteLength;
- for (let i = container.levels.length - 1; i >= 0; i--) {
- // Level padding.
- if (levelDataByteOffset % levelAlign) {
- const paddingBytes = getPadding(levelDataByteOffset, levelAlign);
- levelData.push(new Uint8Array(paddingBytes));
- levelDataByteOffset += paddingBytes;
- }
-
- // Level data.
- const level = container.levels[i];
- levelData.push(level.levelData);
- levelDataByteOffsets[i] = levelDataByteOffset;
- levelDataByteOffset += level.levelData.byteLength;
- }
-
- // Level index is ordered large → small.
- for (let i = 0; i < container.levels.length; i++) {
- const level = container.levels[i];
- levelIndex.setBigUint64(i * 24 + 0, BigInt(levelDataByteOffsets[i]), true);
- levelIndex.setBigUint64(i * 24 + 8, BigInt(level.levelData.byteLength), true);
- levelIndex.setBigUint64(i * 24 + 16, BigInt(level.uncompressedByteLength), true);
- }
-
- ///////////////////////////////////////////////////
- // Header.
- ///////////////////////////////////////////////////
-
- const headerBuffer = new ArrayBuffer(HEADER_BYTE_LENGTH);
- const headerView = new DataView(headerBuffer);
- headerView.setUint32(0, container.vkFormat, true);
- headerView.setUint32(4, container.typeSize, true);
- headerView.setUint32(8, container.pixelWidth, true);
- headerView.setUint32(12, container.pixelHeight, true);
- headerView.setUint32(16, container.pixelDepth, true);
- headerView.setUint32(20, container.layerCount, true);
- headerView.setUint32(24, container.faceCount, true);
- headerView.setUint32(28, container.levelCount, true);
- headerView.setUint32(32, container.supercompressionScheme, true);
-
- headerView.setUint32(36, dfdByteOffset, true);
- headerView.setUint32(40, dfdBuffer.byteLength, true);
- headerView.setUint32(44, kvdByteOffset, true);
- headerView.setUint32(48, kvdBuffer.byteLength, true);
- headerView.setBigUint64(52, BigInt(sgdBuffer.byteLength > 0 ? sgdByteOffset : 0), true);
- headerView.setBigUint64(60, BigInt(sgdBuffer.byteLength), true);
-
- ///////////////////////////////////////////////////
- // Compose.
- ///////////////////////////////////////////////////
-
- return new Uint8Array(
- concat([
- new Uint8Array(KTX2_ID).buffer,
- headerBuffer,
- levelIndex.buffer,
- dfdBuffer,
- kvdBuffer,
- sgdByteOffset > 0
- ? new ArrayBuffer(sgdByteOffset - (kvdByteOffset + kvdBuffer.byteLength)) // align(8)
- : new ArrayBuffer(0),
- sgdBuffer,
- ...levelData,
- ]),
- );
- }
|