dds.ts•4.44 kB
import sharp from 'sharp';
const DDS_MAGIC = 0x20534444; // "DDS "
const DDPF_FOURCC = 0x4;
const FOURCC_DXT5 = 0x35545844; // "DXT5"
interface DDSHeader {
width: number;
height: number;
mipMapCount: number;
pixelFormat: {
flags: number;
fourCC: number;
};
}
function readHeader(buffer: Buffer): DDSHeader {
const magic = buffer.readUInt32LE(0);
if (magic !== DDS_MAGIC) {
throw new Error('Invalid DDS file');
}
return {
height: buffer.readUInt32LE(12),
width: buffer.readUInt32LE(16),
mipMapCount: buffer.readUInt32LE(28),
pixelFormat: {
flags: buffer.readUInt32LE(80),
fourCC: buffer.readUInt32LE(84),
},
};
}
function decodeDXT5Block(block: Buffer, output: Uint8Array, outOffset: number, stride: number): void {
// Alpha block (8 bytes)
const alpha0 = block[0];
const alpha1 = block[1];
const alphaBits =
block[2] | (block[3] << 8) | (block[4] << 16) |
(block[5] << 24) | ((block[6] | (block[7] << 8)) * 0x100000000);
const alphaTable = new Uint8Array(8);
alphaTable[0] = alpha0;
alphaTable[1] = alpha1;
if (alpha0 > alpha1) {
for (let i = 2; i < 8; i++) {
alphaTable[i] = Math.round(((8 - i) * alpha0 + (i - 1) * alpha1) / 7);
}
} else {
for (let i = 2; i < 6; i++) {
alphaTable[i] = Math.round(((6 - i) * alpha0 + (i - 1) * alpha1) / 5);
}
alphaTable[6] = 0;
alphaTable[7] = 255;
}
// Color block (8 bytes starting at offset 8)
const c0 = block.readUInt16LE(8);
const c1 = block.readUInt16LE(10);
const colorBits = block.readUInt32LE(12);
const colors = new Uint8Array(16);
// Decode RGB565
colors[0] = ((c0 >> 11) & 0x1F) * 255 / 31;
colors[1] = ((c0 >> 5) & 0x3F) * 255 / 63;
colors[2] = (c0 & 0x1F) * 255 / 31;
colors[3] = 255;
colors[4] = ((c1 >> 11) & 0x1F) * 255 / 31;
colors[5] = ((c1 >> 5) & 0x3F) * 255 / 63;
colors[6] = (c1 & 0x1F) * 255 / 31;
colors[7] = 255;
// Interpolated colors
colors[8] = (2 * colors[0] + colors[4]) / 3;
colors[9] = (2 * colors[1] + colors[5]) / 3;
colors[10] = (2 * colors[2] + colors[6]) / 3;
colors[11] = 255;
colors[12] = (colors[0] + 2 * colors[4]) / 3;
colors[13] = (colors[1] + 2 * colors[5]) / 3;
colors[14] = (colors[2] + 2 * colors[6]) / 3;
colors[15] = 255;
// Decode 4x4 block
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
const pixelIndex = row * 4 + col;
const colorIndex = (colorBits >> (pixelIndex * 2)) & 0x3;
const alphaIndex = Number((BigInt(alphaBits) >> BigInt(pixelIndex * 3)) & 0x7n);
const outPos = outOffset + row * stride + col * 4;
output[outPos] = colors[colorIndex * 4];
output[outPos + 1] = colors[colorIndex * 4 + 1];
output[outPos + 2] = colors[colorIndex * 4 + 2];
output[outPos + 3] = alphaTable[alphaIndex];
}
}
}
function decodeDXT5(buffer: Buffer, width: number, height: number): Uint8Array {
const output = new Uint8Array(width * height * 4);
const blocksX = Math.ceil(width / 4);
const blocksY = Math.ceil(height / 4);
const stride = width * 4;
let blockOffset = 0;
for (let by = 0; by < blocksY; by++) {
for (let bx = 0; bx < blocksX; bx++) {
const block = buffer.subarray(blockOffset, blockOffset + 16);
const outOffset = by * 4 * stride + bx * 4 * 4;
decodeDXT5Block(block, output, outOffset, stride);
blockOffset += 16;
}
}
return output;
}
export async function decodeDDS(buffer: Buffer): Promise<Buffer> {
const header = readHeader(buffer);
if (!(header.pixelFormat.flags & DDPF_FOURCC)) {
throw new Error('DDS file does not use FourCC format');
}
if (header.pixelFormat.fourCC !== FOURCC_DXT5) {
throw new Error(`Unsupported DDS format: ${header.pixelFormat.fourCC.toString(16)}`);
}
const dataOffset = 128; // Standard DDS header size
const compressedData = buffer.subarray(dataOffset);
const rawPixels = decodeDXT5(compressedData, header.width, header.height);
return sharp(rawPixels, {
raw: {
width: header.width,
height: header.height,
channels: 4,
},
})
.png()
.toBuffer();
}
export function getDDSInfo(buffer: Buffer): { width: number; height: number; format: string } {
const header = readHeader(buffer);
return {
width: header.width,
height: header.height,
format: header.pixelFormat.fourCC === FOURCC_DXT5 ? 'DXT5' : 'unknown',
};
}