"use strict"; // Texture decoding for the UI Image Set editor: DDS (DXT1/DXT3/DXT5, 32/24/16-bit RGB masks) and // TGA (true-colour, RLE) → ImageData. BMP/PNG/JPG go through the browser (createImageBitmap). const TexDecode = (() => { const rgb565 = (c) => [((c >> 11) & 31) * 255 / 31, ((c >> 5) & 63) * 255 / 63, (c & 31) * 255 / 31]; /** One 4×4 DXT colour block → 16 RGB(A) values into out (at pixel x,y). */ function colorBlock(src, o, out, w, h, x, y, dxt1, alphaSrc) { const c0 = src[o] | (src[o + 1] << 8); const c1 = src[o + 2] | (src[o + 3] << 8); const p0 = rgb565(c0); const p1 = rgb565(c1); const pal = [p0, p1]; if (!dxt1 || c0 > c1) { pal.push([(2 * p0[0] + p1[0]) / 3, (2 * p0[1] + p1[1]) / 3, (2 * p0[2] + p1[2]) / 3]); pal.push([(p0[0] + 2 * p1[0]) / 3, (p0[1] + 2 * p1[1]) / 3, (p0[2] + 2 * p1[2]) / 3]); } else { pal.push([(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2, (p0[2] + p1[2]) / 2]); pal.push(null); // transparent black (DXT1 1-bit alpha) } const bits = src[o + 4] | (src[o + 5] << 8) | (src[o + 6] << 16) | (src[o + 7] << 24); for (let i = 0; i < 16; i++) { const px = x + (i & 3); const py = y + (i >> 2); if (px >= w || py >= h) continue; const c = pal[(bits >>> (2 * i)) & 3]; const d = (py * w + px) * 4; if (!c) { out[d] = out[d + 1] = out[d + 2] = out[d + 3] = 0; continue; } out[d] = c[0]; out[d + 1] = c[1]; out[d + 2] = c[2]; out[d + 3] = alphaSrc ? alphaSrc[i] : 255; } } function dxt(src, offset, w, h, kind) { const out = new Uint8ClampedArray(w * h * 4); const blockBytes = kind === "DXT1" ? 8 : 16; let o = offset; const alpha = new Uint8Array(16); for (let y = 0; y < h; y += 4) { for (let x = 0; x < w; x += 4) { let a = null; if (kind === "DXT3") { for (let i = 0; i < 8; i++) { const b = src[o + i]; alpha[i * 2] = (b & 15) * 17; alpha[i * 2 + 1] = (b >> 4) * 17; } a = alpha; } else if (kind === "DXT5") { const a0 = src[o]; const a1 = src[o + 1]; const pal = [a0, a1]; if (a0 > a1) for (let i = 1; i < 7; i++) pal.push(((7 - i) * a0 + i * a1) / 7); else { for (let i = 1; i < 5; i++) pal.push(((5 - i) * a0 + i * a1) / 5); pal.push(0, 255); } // 48 bits of 3-bit indices let lo = src[o + 2] | (src[o + 3] << 8) | (src[o + 4] << 16); let hi = src[o + 5] | (src[o + 6] << 8) | (src[o + 7] << 16); for (let i = 0; i < 8; i++) alpha[i] = pal[(lo >> (3 * i)) & 7]; for (let i = 0; i < 8; i++) alpha[8 + i] = pal[(hi >> (3 * i)) & 7]; a = alpha; } colorBlock(src, o + (kind === "DXT1" ? 0 : 8), out, w, h, x, y, kind === "DXT1", a); o += blockBytes; } } return new ImageData(out, w, h); } const shiftOf = (mask) => { if (!mask) return [0, 0]; let s = 0; while (!((mask >>> s) & 1)) s++; let n = 0; while ((mask >>> (s + n)) & 1) n++; return [s, n]; }; function masked(src, offset, w, h, bpp, masks, hasAlpha) { const out = new Uint8ClampedArray(w * h * 4); const bytes = bpp / 8; const pitch = w * bytes; const chans = masks.map(shiftOf); const dv = new DataView(src.buffer, src.byteOffset, src.byteLength); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const o = offset + y * pitch + x * bytes; const v = bytes === 4 ? dv.getUint32(o, true) : bytes === 3 ? src[o] | (src[o + 1] << 8) | (src[o + 2] << 16) : dv.getUint16(o, true); const d = (y * w + x) * 4; for (let c = 0; c < 3; c++) { const [s, n] = chans[c]; out[d + c] = n ? (((v >>> s) & ((1 << n) - 1)) * 255) / ((1 << n) - 1) : 0; } const [as, an] = chans[3]; out[d + 3] = hasAlpha && an ? (((v >>> as) & ((1 << an) - 1)) * 255) / ((1 << an) - 1) : 255; } } return new ImageData(out, w, h); } function dds(bytes) { const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); if (dv.getUint32(0, true) !== 0x20534444) throw new Error("Not a DDS file"); const h = dv.getUint32(12, true); const w = dv.getUint32(16, true); const pf = dv.getUint32(80, true); const fourCC = String.fromCharCode(bytes[84], bytes[85], bytes[86], bytes[87]); const offset = 128; if (pf & 4) { if (fourCC === "DXT1" || fourCC === "DXT3" || fourCC === "DXT5") return dxt(bytes, offset, w, h, fourCC); if (fourCC === "DXT2" || fourCC === "DXT4") return dxt(bytes, offset, w, h, fourCC === "DXT2" ? "DXT3" : "DXT5"); throw new Error(`DDS format ${fourCC} isn't supported`); } const bpp = dv.getUint32(88, true); const masks = [dv.getUint32(92, true), dv.getUint32(96, true), dv.getUint32(100, true), dv.getUint32(104, true)]; return masked(bytes, offset, w, h, bpp, masks, !!(pf & 1)); } function tga(bytes) { const idLen = bytes[0]; const type = bytes[2]; const w = bytes[12] | (bytes[13] << 8); const h = bytes[14] | (bytes[15] << 8); const bpp = bytes[16]; const topDown = (bytes[17] & 0x20) !== 0; if (![2, 10].includes(type) || ![24, 32].includes(bpp)) throw new Error("Only true-colour TGA (24/32-bit) is supported"); const px = bpp / 8; const out = new Uint8ClampedArray(w * h * 4); let o = 18 + idLen; let i = 0; const put = (b, g, r, a) => { const x = i % w; const y = Math.floor(i / w); const d = ((topDown ? y : h - 1 - y) * w + x) * 4; out[d] = r; out[d + 1] = g; out[d + 2] = b; out[d + 3] = a; i++; }; while (i < w * h) { if (type === 2) { put(bytes[o], bytes[o + 1], bytes[o + 2], px === 4 ? bytes[o + 3] : 255); o += px; continue; } const hdr = bytes[o++]; const n = (hdr & 127) + 1; if (hdr & 128) { const c = [bytes[o], bytes[o + 1], bytes[o + 2], px === 4 ? bytes[o + 3] : 255]; o += px; for (let k = 0; k < n; k++) put(...c); } else { for (let k = 0; k < n; k++) { put(bytes[o], bytes[o + 1], bytes[o + 2], px === 4 ? bytes[o + 3] : 255); o += px; } } } return new ImageData(out, w, h); } /** Any supported texture → ImageBitmap. */ async function decode(bytes, ext) { const e = (ext || "").toLowerCase(); if (e === "dds") return createImageBitmap(dds(bytes)); if (e === "tga") return createImageBitmap(tga(bytes)); return createImageBitmap(new Blob([bytes])); } return { decode, dds, tga }; })(); window.TexDecode = TexDecode;