"use strict"; // DTF – Dusk Texture Format // // Header (13 bytes): // [0–2] "DTF" magic // [3] 0x01 version // [4–7] uint32 width (little-endian) // [8–11] uint32 height (little-endian) // [12] uint8 format // // Formats: // 0x01 Alpha – 1 byte per pixel (alpha channel only) // 0x03 RGB – 3 bytes per pixel (no alpha) // 0x04 RGBA – 4 bytes per pixel // // Followed by width × height × bpp bytes of tightly-packed pixel data. const DTF = (() => { const MAGIC = [0x44, 0x54, 0x46]; // "DTF" const VERSION = 0x01; const FORMAT_ALPHA = 0x01; const FORMAT_RGB = 0x03; const FORMAT_RGBA = 0x04; const HEADER_SIZE = 13; // Bytes per pixel for each format. const BPP = { [FORMAT_ALPHA]: 1, [FORMAT_RGB]: 3, [FORMAT_RGBA]: 4, }; // Encode RGBA source pixels into a DTF ArrayBuffer at the given format. // When format is FORMAT_ALPHA and redAsAlpha is true, the red channel is // used as the alpha value instead of the actual alpha channel. function encode(width, height, rgbaData, format, redAsAlpha) { if(format === undefined) format = FORMAT_RGBA; const bpp = BPP[format]; if(bpp === undefined) throw new Error(`Unknown DTF format: 0x${format.toString(16)}`); const src = rgbaData instanceof Uint8ClampedArray ? rgbaData : new Uint8ClampedArray(rgbaData); const buf = new ArrayBuffer(HEADER_SIZE + width * height * bpp); const bytes = new Uint8Array(buf); const view = new DataView(buf); bytes[0] = MAGIC[0]; bytes[1] = MAGIC[1]; bytes[2] = MAGIC[2]; bytes[3] = VERSION; view.setUint32(4, width, true); view.setUint32(8, height, true); bytes[12] = format; let dst = HEADER_SIZE; for(let i = 0; i < width * height; i++) { const o = i * 4; switch (format) { case FORMAT_ALPHA: bytes[dst++] = redAsAlpha ? src[o] : src[o + 3]; break; case FORMAT_RGB: bytes[dst++] = src[o]; bytes[dst++] = src[o + 1]; bytes[dst++] = src[o + 2]; break; default: // FORMAT_RGBA bytes[dst++] = src[o]; bytes[dst++] = src[o + 1]; bytes[dst++] = src[o + 2]; bytes[dst++] = src[o + 3]; } } return buf; } // Decode a DTF ArrayBuffer. Always returns RGBA pixel data for internal use. // Alpha-format files decode as {R=0, G=0, B=0, A=alpha} so the alpha channel // is preserved and toImageData() can display it correctly. function decode(buffer) { const bytes = new Uint8Array(buffer); const view = new DataView(buffer); if(bytes.length < HEADER_SIZE) throw new Error("File too small to be a valid DTF"); if(bytes[0] !== MAGIC[0] || bytes[1] !== MAGIC[1] || bytes[2] !== MAGIC[2]) { throw new Error("Invalid DTF magic bytes – not a DTF file"); } const version = bytes[3]; if(version !== VERSION) { throw new Error(`Unsupported DTF version: 0x${version.toString(16).padStart(2, "0")}`); } const width = view.getUint32(4, true); const height = view.getUint32(8, true); const format = bytes[12]; const bpp = BPP[format]; if(bpp === undefined) { throw new Error(`Unsupported DTF format: 0x${format.toString(16).padStart(2, "0")}`); } const expected = HEADER_SIZE + width * height * bpp; if(bytes.length < expected) { throw new Error(`DTF pixel data truncated (expected ${expected} bytes, got ${bytes.length})`); } const rgba = new Uint8ClampedArray(width * height * 4); let src = HEADER_SIZE; for(let i = 0; i < width * height; i++) { const o = i * 4; switch (format) { case FORMAT_ALPHA: rgba[o] = rgba[o + 1] = rgba[o + 2] = 0; rgba[o + 3] = bytes[src++]; break; case FORMAT_RGB: rgba[o] = bytes[src++]; rgba[o + 1] = bytes[src++]; rgba[o + 2] = bytes[src++]; rgba[o + 3] = 255; break; default: // FORMAT_RGBA rgba[o] = bytes[src++]; rgba[o + 1] = bytes[src++]; rgba[o + 2] = bytes[src++]; rgba[o + 3] = bytes[src++]; } } return { width, height, format, data: rgba }; } // Convert RGBA source pixels to a display-ready ImageData for the given format. // Shows exactly how the texture will look after a DTF encode/decode round-trip. // Alpha → grayscale from alpha channel (or red if redAsAlpha), out-alpha=255 // RGB → discard alpha, fully opaque // RGBA → pass-through function toImageData(width, height, rgbaData, format, redAsAlpha) { const src = rgbaData instanceof Uint8ClampedArray ? rgbaData : new Uint8ClampedArray(rgbaData); const out = new Uint8ClampedArray(width * height * 4); for(let i = 0; i < width * height; i++) { const o = i * 4; switch (format) { case FORMAT_ALPHA: { const v = redAsAlpha ? src[o] : src[o + 3]; out[o] = out[o + 1] = out[o + 2] = v; out[o + 3] = v; // use value as canvas alpha so background shows through transparent areas break; } case FORMAT_RGB: out[o] = src[o]; out[o + 1] = src[o + 1]; out[o + 2] = src[o + 2]; out[o + 3] = 255; break; default: // FORMAT_RGBA out[o] = src[o]; out[o + 1] = src[o + 1]; out[o + 2] = src[o + 2]; out[o + 3] = src[o + 3]; } } return new ImageData(out, width, height); } return Object.freeze({ encode, decode, toImageData, FORMAT_ALPHA, FORMAT_RGB, FORMAT_RGBA, VERSION, HEADER_SIZE, BPP, }); })();