/*******************************************************************************
* Copyright (C) 2013 JMaNGOS <http://jmangos.org/>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
package org.jmangos.tools.blp;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
/**
* References: http://madx.dk/wowdev/wiki/index.php?title=BLP
* http://forum.worldwindcentral.com/showthread.php?p=71605
* http://en.wikipedia.org/wiki/S3_Texture_Compression
* http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.
* txt
* http://msdn.microsoft.com/en-us/library/bb147243%28VS.85%29.aspx
*
* This code is not at all optimized for performance nor for cleanliness.
*
* ---
*
* @author Dan Watling <dan@synaptik.com>
**/
public class BLP {
/**
* Minimal header length
*/
private static final int HEADER_LENGTH = 20;
private byte[] signature = new byte[4]; // BLP2
private int type; // 0 = JPEG compression; 1 =
// Uncompressed or DirectX
// compression
private byte encoding; // 1 for uncompressed, 2 for
// DirectX compression
private byte alphaBitDepth; // 0, 1 or 8
private byte alphaEncoding; // 0 = DirectX 1 alpha (0/1
// bit);
// 1
// = DirectX 2/3 alpha (4-bit
// alpha); 2 = DirectX 4/5
// alpha
// (interpolated)
private byte hasMips; // 0 = no mip maps; 1 = has
// mips
// (#
// determined by dimensions)
private int width;
private int height;
private int[] mipmapOffsets = new int[16];
private int[] mipmapSize = new int[16];
private Color[] palette = new Color[256];
private List<byte[]> mipmaps = new ArrayList<byte[]>();
public static BLP read(final ByteBuffer bb) {
if (bb.capacity() < HEADER_LENGTH) {
return null;
}
bb.order(ByteOrder.LITTLE_ENDIAN); // format is in little endian, ensure
// the buffer is too
final BLP result = new BLP();
bb.get(result.signature);
result.type = bb.getInt();
result.encoding = bb.get();
result.alphaBitDepth = bb.get();
result.alphaEncoding = bb.get();
result.hasMips = bb.get();
result.width = bb.getInt();
result.height = bb.getInt();
for (int index = 0; index < result.mipmapOffsets.length; index++) {
result.mipmapOffsets[index] = bb.getInt();
}
for (int index = 0; index < result.mipmapSize.length; index++) {
result.mipmapSize[index] = bb.getInt();
}
for (int index = 0; index < 256; index++) {
final byte b = bb.get();
final byte g = bb.get();
final byte r = bb.get();
bb.get(); // toss alpha value
result.palette[index] = new Color(r, g, b); // RGB
}
readMipMaps(result, bb);
return result;
}
@Override
public final String toString() {
return "(" +
this.width +
"x" +
this.height +
") " +
this.type +
"/" +
this.encoding +
"/" +
this.alphaBitDepth +
"/" +
this.alphaEncoding;
}
private static void readMipMaps(final BLP result, final ByteBuffer bb) {
for (int index = 0; index < result.mipmapOffsets.length; index++) {
if ((result.mipmapOffsets[index] > 0) && (result.mipmapOffsets[index] < bb.capacity())) {
bb.position(result.mipmapOffsets[index]);
if ((bb.position() + result.mipmapSize[index]) > bb.capacity()) {
continue;
}
final byte[] data = new byte[result.mipmapSize[index]];
bb.get(data);
result.mipmaps.add(data);
}
}
}
public final BufferedImage getBufferedImage() {
if (this.mipmaps.size() == 0) {
return null;
}
final ByteBuffer bb = ByteBuffer.wrap(this.mipmaps.get(0));
bb.order(ByteOrder.LITTLE_ENDIAN);
BufferedImage result = null;
if ((this.type == 1) && (this.encoding == 1)) {
if (this.alphaBitDepth == 0) {
result = getBufferedImageUncompressed(bb, this.alphaBitDepth);
} else if (this.alphaBitDepth == 1) {
result = getBufferedImageUncompressed(bb, this.alphaBitDepth);
} else if (this.alphaBitDepth == 8) {
result = getBufferedImageUncompressed(bb, this.alphaBitDepth);
}
} else if ((this.type == 1) && (this.encoding == 2)) {
if ((this.alphaBitDepth == 0) && (this.alphaEncoding == 0)) {
// DTX1 (no alpha)
result = getBufferedImageDXT1(bb, false);
} else if ((this.alphaBitDepth == 1) && (this.alphaEncoding == 0)) {
// DXT1 (alpha)
result = getBufferedImageDXT1(bb, true);
} else if ((this.alphaBitDepth == 8) && (this.alphaEncoding == 1)) {
// DTX3
result = getBufferedImageDXT3(bb);
} else if ((this.alphaBitDepth == 8) && (this.alphaEncoding == 7)) {
// DXT5
result = getBufferedImageDXT5(bb);
}
}
return result;
}
protected final BufferedImage getBufferedImageUncompressed(final ByteBuffer bb,
final int givenAlphaBitDepth) {
final BufferedImage result =
new BufferedImage(this.width, this.height, BufferedImage.TYPE_INT_ARGB_PRE);
final int[][] colors = new int[this.height][this.width];
final int[][] alpha = new int[this.height][this.width];
for (int y = 0; y < this.height; y++) {
for (int x = 0; x < this.width; x++) {
final byte paletteIndex = bb.get();
int pIndex = paletteIndex;
if (paletteIndex < 0) {
pIndex = 256 + paletteIndex;
}
colors[y][x] = this.palette[pIndex].asInt();
}
}
for (int y = 0; y < this.height; y++) {
for (int x = 0; x < this.width; x++) {
if (givenAlphaBitDepth == 0) {
alpha[y][x] = 0xFF << 24;
} else if (givenAlphaBitDepth == 1) {
final byte b = bb.get();
int actualB = b;
if (actualB < 0) {
actualB = 256 + b;
}
for (int bit = 0; bit < 8; bit++) {
alpha[y][x + bit] = (255 * ((actualB >> bit) & 0x01)) << 24;
}
x += 7;
} else {
final byte b = bb.get();
int actualB = b;
if (actualB < 0) {
actualB = 256 + b;
}
alpha[y][x] = actualB << 24;
}
}
}
for (int y = 0; y < this.height; y++) {
for (int x = 0; x < this.width; x++) {
result.setRGB(x, y, colors[y][x] + alpha[y][x]);
}
}
return result;
}
/**
* DXT1 doesn't look right. Alpha is odd (INV_Misc_Bag_09_Blue.blp)
*
* @param bb
* @param alpha
* @return
*/
protected BufferedImage getBufferedImageDXT1(final ByteBuffer bb, final boolean alpha) {
final int[] pixels = new int[16];
final BufferedImage result =
new BufferedImage(this.width, this.height, BufferedImage.TYPE_INT_ARGB_PRE);
final int numTilesWide = this.width / 4;
final int numTilesHigh = this.height / 4;
for (int i = 0; i < numTilesHigh; i++) {
for (int j = 0; j < numTilesWide; j++) {
final short c0 = bb.getShort();
final short c1 = bb.getShort();
int uC0 = c0;
int uC1 = c1;
if (uC0 < 0) {
uC0 = 65536 + c0;
}
if (uC1 < 0) {
uC1 = 65536 + c1;
}
final Color[] lookupTable = expandLookupTableDXT1(c0, c1, alpha);
final int colorData = bb.getInt();
for (int k = pixels.length - 1; k >= 0; k--) {
final int colorCode = (colorData >>> (k * 2)) & 0x03;
int alphaValue = 255;
if (alpha && (colorCode == 3) && (uC0 < uC1)) {
alphaValue = 0;
}
pixels[k] = (alphaValue << 24) | getPixel888(lookupTable[colorCode]);
}
result.setRGB(j * 4, i * 4, 4, 4, pixels, 0, 4);
}
}
return result;
}
// Yanked from http://forum.worldwindcentral.com/showthread.php?p=71605
protected BufferedImage getBufferedImageDXT3(final ByteBuffer bb) {
final int[] pixels = new int[16];
final int[] alphas = new int[16];
final BufferedImage result =
new BufferedImage(this.width, this.height, BufferedImage.TYPE_INT_ARGB_PRE);
final int numTilesWide = this.width / 4;
final int numTilesHigh = this.height / 4;
for (int i = 0; i < numTilesHigh; i++) {
for (int j = 0; j < numTilesWide; j++) {
// Read the alpha table.
final long alphaData = bb.getLong();
for (int k = alphas.length - 1; k >= 0; k--) {
alphas[k] = (int) (alphaData >>> (k * 4)) & 0xF; // Alphas
// are just
// 4 bits
// per
// pixel
alphas[k] <<= 4;
}
final short minColor = bb.getShort();
final short maxColor = bb.getShort();
final Color[] lookupTable = expandLookupTableDXT3(minColor, maxColor);
final int colorData = bb.getInt();
for (int k = pixels.length - 1; k >= 0; k--) {
final int colorCode = (colorData >>> (k * 2)) & 0x03;
pixels[k] = (alphas[k] << 24) | getPixel888(lookupTable[colorCode]);
}
result.setRGB(j * 4, i * 4, 4, 4, pixels, 0, 4);
}
}
return result;
}
protected BufferedImage getBufferedImageDXT5(final ByteBuffer bb) {
final int[] pixels = new int[16];
final int[] alphas = new int[16];
final BufferedImage result =
new BufferedImage(this.width, this.height, BufferedImage.TYPE_INT_ARGB_PRE);
final int numTilesWide = this.width / 4;
final int numTilesHigh = this.height / 4;
for (int i = 0; i < numTilesHigh; i++) {
for (int j = 0; j < numTilesWide; j++) {
final long data = bb.getLong();
final short a0 = (short) (data & 0xFF);
final short a1 = (short) ((data >> 8) & 0xFF);
final int[] alphaTable = expandAlphaTable(a0, a1);
long alphaData = data >>> 16;
for (int k = 0; k < alphas.length; k++) {
final int alphaIndex = (int) (alphaData & 0x07);
alphas[k] = alphaTable[alphaIndex];
alphaData >>>= 3;
}
final short minColor = bb.getShort();
final short maxColor = bb.getShort();
final Color[] lookupTable = expandLookupTableDXT3(minColor, maxColor);
final int colorData = bb.getInt();
for (int k = pixels.length - 1; k >= 0; k--) {
final int colorCode = (colorData >>> (k * 2)) & 0x03;
pixels[k] = (alphas[k] << 24) | getPixel888(lookupTable[colorCode]);
}
result.setRGB(j * 4, i * 4, 4, 4, pixels, 0, 4);
}
}
return result;
}
// Yanked from http://forum.worldwindcentral.com/showthread.php?p=71605
protected static Color getColor565(final int pixel) {
final Color color = new Color();
color.r = (int) (((long) pixel) & 0xf800) >>> 8;
color.g = (int) (((long) pixel) & 0x07e0) >>> 3;
color.b = (int) (((long) pixel) & 0x001f) << 3;
return color;
}
protected static Color getColor555(final int pixel) {
final Color color = new Color();
color.r = (int) (((long) pixel) & 0xf800) >>> 8;
color.g = (int) (((long) pixel) & 0x07c0) >>> 3;
color.b = (int) (((long) pixel) & 0x001f) << 3;
return color;
}
private static int[] expandAlphaTable(final short a0, final short a1) {
final int[] a = new int[] { a0, a1, 0, 0, 0, 0, 0, 0 };
if (a[0] > a[1]) {
a[2] = ((6 * a[0]) + (1 * a[1]) + 3) / 7;
a[3] = ((5 * a[0]) + (2 * a[1]) + 3) / 7;
a[4] = ((4 * a[0]) + (3 * a[1]) + 3) / 7;
a[5] = ((3 * a[0]) + (4 * a[1]) + 3) / 7;
a[6] = ((2 * a[0]) + (5 * a[1]) + 3) / 7;
a[7] = ((1 * a[0]) + (6 * a[1]) + 3) / 7;
} else {
a[2] = ((4 * a[0]) + (1 * a[1]) + 2) / 5;
a[3] = ((3 * a[0]) + (2 * a[1]) + 2) / 5;
a[4] = ((2 * a[0]) + (3 * a[1]) + 2) / 5;
a[5] = ((1 * a[0]) + (4 * a[1]) + 2) / 5;
a[6] = 0;
a[7] = 255;
}
return a;
}
// Yanked from http://forum.worldwindcentral.com/showthread.php?p=71605
private static Color[] expandLookupTableDXT3(final short c0, final short c1) {
final Color[] c =
new Color[] { getColor565(c0), getColor565(c1), new Color(), new Color() };
c[2].r = ((2 * c[0].r) + c[1].r + 1) / 3;
c[2].g = ((2 * c[0].g) + c[1].g + 1) / 3;
c[2].b = ((2 * c[0].b) + c[1].b + 1) / 3;
c[3].r = (c[0].r + (2 * c[1].r) + 1) / 3;
c[3].g = (c[0].g + (2 * c[1].g) + 1) / 3;
c[3].b = (c[0].b + (2 * c[1].b) + 1) / 3;
return c;
}
private static Color[] expandLookupTableDXT1(final short c0, final short c1, final boolean alpha) {
int uC0 = c0;
int uC1 = c1;
if (uC0 < 0) {
uC0 = 65536 + c0;
}
if (uC1 < 0) {
uC1 = 65536 + c1;
}
final Color[] c =
new Color[] { getColor565(c0), getColor565(c1), new Color(), new Color() };
if ((alpha && (uC0 > uC1)) || !alpha) {
c[2].r = ((2 * c[0].r) + c[1].r + 1) / 3;
c[2].g = ((2 * c[0].g) + c[1].g + 1) / 3;
c[2].b = ((2 * c[0].b) + c[1].b + 1) / 3;
c[3].r = (c[0].r + 1 + (2 * c[1].r)) / 3;
c[3].g = (c[0].g + 1 + (2 * c[1].g)) / 3;
c[3].b = (c[0].b + 1 + (2 * c[1].b)) / 3;
} else {
c[2].r = (c[0].r + c[1].r + 1) / 2;
c[2].g = (c[0].g + c[1].g + 1) / 2;
c[2].b = (c[0].b + c[1].b + 1) / 2;
c[3].r = 0;
c[3].g = 0;
c[3].b = 0;
}
return c;
}
// Yanked from http://forum.worldwindcentral.com/showthread.php?p=71605
protected static int getPixel888(final Color color) {
final int r = color.r;
final int g = color.g;
final int b = color.b;
return (r << 16) | (g << 8) | b;
}
/**
* @return the signature
*/
public final byte[] getSignature() {
return this.signature;
}
/**
* @param signature
* the signature to set
*/
public final void setSignature(final byte[] signature) {
this.signature = signature;
}
/**
* @return the type
*/
public final int getType() {
return this.type;
}
/**
* @param type
* the type to set
*/
public final void setType(final int type) {
this.type = type;
}
/**
* @return the encoding
*/
public final byte getEncoding() {
return this.encoding;
}
/**
* @param encoding
* the encoding to set
*/
public final void setEncoding(final byte encoding) {
this.encoding = encoding;
}
/**
* @return the alphaBitDepth
*/
public final byte getAlphaBitDepth() {
return this.alphaBitDepth;
}
/**
* @param alphaBitDepth
* the alphaBitDepth to set
*/
public final void setAlphaBitDepth(final byte alphaBitDepth) {
this.alphaBitDepth = alphaBitDepth;
}
/**
* @return the alphaEncoding
*/
public final byte getAlphaEncoding() {
return this.alphaEncoding;
}
/**
* @param alphaEncoding
* the alphaEncoding to set
*/
public final void setAlphaEncoding(final byte alphaEncoding) {
this.alphaEncoding = alphaEncoding;
}
/**
* @return the hasMips
*/
public final byte getHasMips() {
return this.hasMips;
}
/**
* @param hasMips
* the hasMips to set
*/
public final void setHasMips(final byte hasMips) {
this.hasMips = hasMips;
}
/**
* @return the width
*/
public final int getWidth() {
return this.width;
}
/**
* @param width
* the width to set
*/
public final void setWidth(final int width) {
this.width = width;
}
/**
* @return the height
*/
public final int getHeight() {
return this.height;
}
/**
* @param height
* the height to set
*/
public final void setHeight(final int height) {
this.height = height;
}
/**
* @return the mipmapOffsets
*/
public final int[] getMipmapOffsets() {
return this.mipmapOffsets;
}
/**
* @param mipmapOffsets
* the mipmapOffsets to set
*/
public final void setMipmapOffsets(final int[] mipmapOffsets) {
this.mipmapOffsets = mipmapOffsets;
}
/**
* @return the mipmapSize
*/
public final int[] getMipmapSize() {
return this.mipmapSize;
}
/**
* @param mipmapSize
* the mipmapSize to set
*/
public final void setMipmapSize(final int[] mipmapSize) {
this.mipmapSize = mipmapSize;
}
/**
* @return the palette
*/
public final Color[] getPalette() {
return this.palette;
}
/**
* @param palette
* the palette to set
*/
public final void setPalette(final Color[] palette) {
this.palette = palette;
}
/**
* @return the mipmaps
*/
public final List<byte[]> getMipmaps() {
return this.mipmaps;
}
/**
* @param mipmaps
* the mipmaps to set
*/
public final void setMipmaps(final List<byte[]> mipmaps) {
this.mipmaps = mipmaps;
}
}