Files
dusk/tools/editor/common/dtf.js
T
2026-04-13 20:03:02 -05:00

176 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
// DTF Dusk Texture Format
//
// Header (13 bytes):
// [02] "DTF" magic
// [3] 0x01 version
// [47] uint32 width (little-endian)
// [811] 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,
});
})();