js-blp
Version:
BLP (Blizzard Texture File) Reader
339 lines (287 loc) • 8.59 kB
JavaScript
/*!
js-blp (https://github.com/Kruithne/js-blp)
Author: Kruithne <kruithne@gmail.com>
License: MIT
*/
const BLPFile = ((Bufo) => {
/**
* Error thrown by the BLPFile class.
* @class BLPError
*/
class BLPError extends Error {
constructor(message, ...args) {
message = 'BLPFile: ' + message.replace(/{(\d+)}/g, (match, number) => {
return typeof args[number] !== 'undefined' ? args[number] : match;
});
super(message);
this.stack = (new Error(message)).stack;
this.name = this.constructor.name;
}
}
return class BLPFile {
/**
* Static constant representing DXT1 compression.
* @returns {number}
*/
static get DXT1() {
return 0x1;
}
/**
* Static constant representing DXT3 compression.
* @returns {number}
*/
static get DXT3() {
return 0x2;
}
/**
* Static constant representing DXT5 compression.
* @returns {number}
*/
static get DXT5() {
return 0x4;
}
/**
* Construct a new BLPFile instance.
* @param {*} data Anything supported by Bufo.
*/
constructor(data) {
this.data = new Bufo(data);
// Check magic value..
if (this.data.readUInt32() !== 0x32504c42)
throw new BLPError('Provided data is not a BLP file (invalid header magic).');
// Check the BLP file type..
let type = this.data.readUInt32();
if (type !== 1)
throw new BLPError('Unsupported BLP type ({0} !== 1)', type);
// Read file flags..
this.encoding = this.data.readUInt8();
this.alphaDepth = this.data.readUInt8();
this.alphaEncoding = this.data.readUInt8();
this.containsMipmaps = this.data.readUInt8();
// Read file dimensions..
this.width = this.data.readUInt32();
this.height = this.data.readUInt32();
// Read mipmap data..
this.mapOffsets = this.data.readUInt32(16);
this.mapSizes = this.data.readUInt32(16);
// Calculate available mipmaps..
this.mapCount = 0;
for (let ofs of this.mapOffsets)
if (ofs !== 0)
this.mapCount++;
// Read colour palette..
this.palette = [];
if (this.encoding === 1)
for (let i = 0; i < 256; i++)
this.palette[i] = this.data.readUInt8(4);
}
/**
* Obtain the pixels for the given mipmap.
* @param {number} [mipmap]
*/
getPixels(mipmap) {
// Constrict the requested mipmap to a valid range..
mipmap = Math.max(0, Math.min(mipmap || 0, this.mapCount - 1));
// Calculate the scaled dimensions..
this.scale = Math.pow(2, mipmap);
this.scaledWidth = this.width / this.scale;
this.scaledHeight = this.height / this.scale;
this.scaledLength = this.scaledWidth * this.scaledHeight;
// Extract the raw data we need..
this.data.seek(this.mapOffsets[mipmap]);
this.rawData = this.data.readUInt8(this.mapSizes[mipmap]);
// Decode the raw data depending on the file..
switch (this.encoding) {
case 1:
return this._getUncompressed();
break;
case 2:
return this._getCompressed();
break;
case 3:
return BLPFile._marshalBGRA(this.rawData);
break;
}
}
/**
* Calculate the alpha using this files alpha depth.
* @param {number} index Alpha index.
* @private
*/
_getAlpha(index) {
let byte;
switch (this.alphaDepth) {
case 1:
byte = this.rawData[this.scaledLength + (index / 8)];
return (byte & (0x01 << (index % 8))) === 0 ? 0x00 : 0xFF;
case 4:
byte = this.rawData[this.scaledLength + (index / 2)];
return (index % 2 === 0 ? (byte & 0x0F) << 4 : byte & 0xF0);
case 8:
return this.rawData[this.scaledLength + index];
default:
return 0xFF;
}
}
/**
* Extract compressed data.
* @private
*/
_getCompressed() {
let flags = this.alphaDepth > 1 ? (this.alphaEncoding === 7 ? BLPFile.DXT5 : BLPFile.DXT3) : BLPFile.DXT1;
let data = [];
let pos = 0;
let blockBytes = (flags & BLPFile.DXT1) !== 0 ? 8 : 16;
let target = [];
for (let y = 0; y < this.scaledHeight; y += 4) {
for (let x = 0; x < this.scaledWidth; x+= 4) {
let blockPos = 0;
if (this.rawData.length === pos)
continue;
let colourIndex = pos;
if ((flags & (BLPFile.DXT3 | BLPFile.DXT5)) !== 0)
colourIndex += 8;
// Decompress colour..
let isDXT1 = (flags & BLPFile.DXT1) !== 0;
let colours = [];
let a = BLPFile._unpackColour(this.rawData, colourIndex, 0, colours, 0);
let b = BLPFile._unpackColour(this.rawData, colourIndex, 2, colours, 4);
for (let i = 0; i < 3; i++) {
let c = colours[i];
let d = colours[i + 4];
if (isDXT1 && a <= b) {
colours[i + 8] = (c + d) / 2;
colours[i + 12] = 0;
} else {
colours[i + 8] = (2 * c + d) / 3;
colours[i + 12] = (c + 2 * d) / 3;
}
}
colours[8 + 3] = 255;
colours[12 + 3] = (isDXT1 && a <= b) ? 0 : 255;
let index = [];
for (let i = 0; i < 4; i++) {
let packed = this.rawData[colourIndex + 4 + i];
index[i * 4] = packed & 0x3;
index[1 + i * 4] = (packed >> 2) & 0x3;
index[2 + i * 4] = (packed >> 4) & 0x3;
index[3 + i * 4] = (packed >> 6) & 0x3;
}
for (let i = 0; i < 16; i++) {
let ofs = index[i] * 4;
target[4 * i] = colours[ofs];
target[4 * i + 1] = colours[ofs + 1];
target[4 * i + 2] = colours[ofs + 2];
target[4 * i + 3] = colours[ofs + 3];
}
if ((flags & BLPFile.DXT3) !== 0) {
for (let i = 0; i < 8; i++) {
let quant = this.rawData[pos + i];
let low = (quant & 0x0F);
let high = (quant & 0xF0);
target[8 * i + 3] = (low | (low << 4));
target[8 * i + 7] = (high | (high >> 4));
}
} else if ((flags & BLPFile.DXT5) !== 0) {
let a0 = this.rawData[pos];
let a1 = this.rawData[pos + 1];
let colours = [];
colours[0] = a0;
colours[1] = a1;
if (a0 <= a1) {
for (let i = 1; i < 5; i++)
colours[i + 1] = (((5 - i) * a0 + i * a1) / 5) | 0;
colours[6] = 0;
colours[7] = 255;
} else {
for (let i = 1; i < 7; i++)
colours[i + 1] = (((7 - i) * a0 + i * a1) / 7) | 0;
}
let indices = [];
let blockPos = 2;
let indicesPos = 0;
for (let i = 0; i < 2; i++) {
let value = 0;
for (let j = 0; j < 3; j++) {
let byte = this.rawData[pos + blockPos++];
value |= (byte << 8 * j);
}
for (let j = 0; j < 8; j++)
indices[indicesPos++] = (value >> 3 * j) & 0x07;
}
for (let i = 0; i < 16; i++)
target[4 * i + 3] = colours[indices[i]];
}
for (let pY = 0; pY < 4; pY++) {
for (let pX = 0; pX < 4; pX++) {
let sX = x + pX;
let sY = y + pY;
if (sX < this.scaledWidth && sY < this.scaledHeight) {
let pixel = 4 * (this.scaledWidth * sY + sX);
for (let i = 0; i < 4; i++)
data[pixel + i] = target[blockPos + i];
}
blockPos += 4;
}
}
pos += blockBytes;
}
}
return new Bufo(data);
}
/**
* Match the uncompressed data with the palette.
* @private
*/
_getUncompressed() {
let buf = new Bufo(this.scaledLength * 4);
for (let i = 0; i < this.scaledLength; i++) {
let colour = this.palette[this.rawData[i]];
buf.writeUInt8([colour[2], colour[1], colour[0], this._getAlpha(i)]);
}
buf.seek(0);
return buf;
}
/**
* Unpack a colour value.
* @param {Array} block
* @param {number} index
* @param {number} ofs
* @param {Array} colour
* @param {number} colourOfs
* @private
*/
static _unpackColour(block, index, ofs, colour, colourOfs) {
let value = block[index + ofs] | (block[index + 1 + ofs] << 8);
let r = (value >> 11) & 0x1F;
let g = (value >> 5) & 0x3F;
let b = value & 0x1F;
colour[colourOfs] = (r << 3) | (r >> 2);
colour[colourOfs + 1] = (g << 2) | (g >> 4);
colour[colourOfs + 2] = (b << 3) | (b >> 2);
colour[colourOfs + 3] = 255;
return value;
}
/**
* Marshal a BGRA array into an RGBA ordered Bufo.
* @param {Array} data
* @returns {Bufo}
* @private
*/
static _marshalBGRA(data) {
let buf = new Bufo(data.length);
let count = data.length / 4;
for (let i = 0; i < count; i++) {
let ofs = i * 4;
buf.writeUInt8([
data[ofs + 2], data[ofs + 1], data[ofs], data[ofs + 3]
]);
}
buf.seek(0);
return buf;
}
}
})(typeof Bufo === 'undefined' ? require('bufo') : Bufo);
// Export to NodeJS.
if (typeof module === 'object' && typeof module.exports === 'object')
module.exports = BLPFile;