Files
2026-10-06 22:25:34 -04:00

63 lines
2.3 KiB
JavaScript

import { createHash } from "node:crypto";
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
/** Validate a legacy DXT1 DDS and describe every source mip without decoding or resampling it. */
export function inspectNativeWaterDxt1Dds(bytes) {
if (!Buffer.isBuffer(bytes)) bytes = Buffer.from(bytes);
if (bytes.length < 128 || bytes.toString("ascii", 0, 4) !== "DDS ") {
throw new Error("Native water frame is not a complete DDS file.");
}
const headerSize = bytes.readUInt32LE(4);
const height = bytes.readUInt32LE(12);
const width = bytes.readUInt32LE(16);
const pixelFormatSize = bytes.readUInt32LE(76);
const formatFourCC = bytes.toString("ascii", 84, 88);
const mipCount = Math.max(1, bytes.readUInt32LE(28));
if (headerSize !== 124 || pixelFormatSize !== 32) {
throw new Error(`Unsupported DDS header sizes ${headerSize}/${pixelFormatSize}.`);
}
if (formatFourCC !== "DXT1") throw new Error(`Unsupported native water frame format ${formatFourCC}.`);
if (width < 1 || height < 1 || width % 4 !== 0 || height % 4 !== 0) {
throw new Error(`Native water DXT1 dimensions must be positive multiples of four; got ${width}x${height}.`);
}
const maxMipCount = 1 + Math.floor(Math.log2(Math.max(width, height)));
if (mipCount > maxMipCount) throw new Error(`DDS declares ${mipCount} mips for ${width}x${height}.`);
let offset = 128;
let mipWidth = width;
let mipHeight = height;
const mips = [];
for (let level = 0; level < mipCount; level += 1) {
const byteLength = Math.ceil(mipWidth / 4) * Math.ceil(mipHeight / 4) * 8;
const end = offset + byteLength;
if (end > bytes.length) throw new Error(`DDS mip ${level} is truncated.`);
const data = bytes.subarray(offset, end);
mips.push({
level,
width: mipWidth,
height: mipHeight,
offset,
byteLength,
sha256: sha256(data),
});
offset = end;
mipWidth = Math.max(1, Math.floor(mipWidth / 2));
mipHeight = Math.max(1, Math.floor(mipHeight / 2));
}
if (offset !== bytes.length) {
throw new Error(`DDS has ${bytes.length - offset} trailing bytes after its declared mip chain.`);
}
return {
width,
height,
mipCount,
formatFourCC,
headerBytes: 128,
fileBytes: bytes.length,
sha256: sha256(bytes),
mips,
exactMipChainConsumption: offset === bytes.length,
};
}