package de.lessvoid.nifty.render.io;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.WillNotClose;
import javax.imageio.ImageIO;
/**
* A utility to load TGAs (NOT THREAD SAFE). Fresh cut of code but largely influenced by the TGA loading class provided
* as part of the Java Monkey Engine (JME). Why not check out what they're doing over at http://www.jmonkeyengine.com.
* Kudos to Mark Powell.
*
* @author Kevin Glass, Julien Gouesse (JOGL 2 port)
*/
public class TGAImageLoader implements ImageLoader {
private int imageWidth;
private int imageHeight;
private short imageBitDepth;
private int textureWidth;
private int textureHeight;
private boolean shouldFlipVertically;
private boolean shouldForceAlpha;
@Override
public int getImageBitDepth() {
return imageBitDepth;
}
@Override
public int getImageWidth() {
return imageWidth;
}
@Override
public int getImageHeight() {
return imageHeight;
}
@Override
public int getTextureWidth() {
return textureWidth;
}
@Override
public int getTextureHeight() {
return textureHeight;
}
@Nonnull
@Override
public ByteBuffer loadAsByteBufferRGBA(@Nonnull @WillNotClose final InputStream imageStream) throws IOException {
return loadImage(imageStream, false, true, null);
}
@Nonnull
@Override
public ByteBuffer loadAsByteBufferARGB(
@Nonnull @WillNotClose final InputStream imageStream,
final boolean shouldFlipVertically) throws IOException {
return loadImage(imageStream, shouldFlipVertically, false, null, false, true);
}
@Nonnull
@Override
public BufferedImage loadAsBufferedImage(@Nonnull @WillNotClose final InputStream imageStream) throws IOException {
return ImageIO.read(imageStream);
}
// Internal implementations
// TODO Refactor.
@SuppressWarnings("ConstantConditions")
@Nonnull
private ByteBuffer loadImage(
@Nonnull @WillNotClose final InputStream imageStream,
final boolean shouldFlipVertically,
final boolean shouldForceAlpha,
@Nullable final int[] transparency) throws IOException {
this.shouldFlipVertically = shouldFlipVertically;
this.shouldForceAlpha = shouldForceAlpha;
if (transparency != null) {
this.shouldForceAlpha = true;
}
byte red;
byte green;
byte blue;
byte alpha;
BufferedInputStream bis = new BufferedInputStream(imageStream, 100000);
DataInputStream dis = new DataInputStream(bis);
// Read in the Header
short idLength = (short) dis.read();
dis.skipBytes(11);
imageWidth = flipEndian(dis.readShort());
imageHeight = flipEndian(dis.readShort());
imageBitDepth = (short) dis.read();
if (imageBitDepth == 32) {
this.shouldForceAlpha = false;
}
textureWidth = get2Fold(imageWidth);
textureHeight = get2Fold(imageHeight);
short imageDescriptor = (short) dis.read();
if ((imageDescriptor & 0x0020) == 0) {
this.shouldFlipVertically = !this.shouldFlipVertically;
}
// Skip image ID
if (idLength > 0) {
long skipped = 0;
while (skipped < idLength) {
skipped += bis.skip(idLength);
}
}
byte[] rawData;
if ((imageBitDepth == 32) || (this.shouldForceAlpha)) {
imageBitDepth = 32;
rawData = new byte[textureWidth * textureHeight * 4];
} else if (imageBitDepth == 24) {
rawData = new byte[textureWidth * textureHeight * 3];
} else {
throw new RuntimeException("Only 24 and 32 bit TGAs are supported");
}
if (imageBitDepth == 24) {
if (this.shouldFlipVertically) {
for (int i = imageHeight - 1; i >= 0; i--) {
for (int j = 0; j < imageWidth; j++) {
blue = dis.readByte();
green = dis.readByte();
red = dis.readByte();
int ofs = ((j + (i * textureWidth)) * 3);
rawData[ofs] = red;
rawData[ofs + 1] = green;
rawData[ofs + 2] = blue;
}
}
} else {
for (int i = 0; i < imageHeight; i++) {
for (int j = 0; j < imageWidth; j++) {
blue = dis.readByte();
green = dis.readByte();
red = dis.readByte();
int ofs = ((j + (i * textureWidth)) * 3);
rawData[ofs] = red;
rawData[ofs + 1] = green;
rawData[ofs + 2] = blue;
}
}
}
} else if (imageBitDepth == 32) {
if (this.shouldFlipVertically) {
for (int i = imageHeight - 1; i >= 0; i--) {
for (int j = 0; j < imageWidth; j++) {
blue = dis.readByte();
green = dis.readByte();
red = dis.readByte();
if (this.shouldForceAlpha) {
alpha = (byte) 255;
} else {
alpha = dis.readByte();
}
int ofs = ((j + (i * textureWidth)) * 4);
rawData[ofs] = red;
rawData[ofs + 1] = green;
rawData[ofs + 2] = blue;
rawData[ofs + 3] = alpha;
if (alpha == 0) {
rawData[ofs + 2] = (byte) 0;
rawData[ofs + 1] = (byte) 0;
rawData[ofs] = (byte) 0;
}
}
}
} else {
for (int i = 0; i < imageHeight; i++) {
for (int j = 0; j < imageWidth; j++) {
blue = dis.readByte();
green = dis.readByte();
red = dis.readByte();
if (this.shouldForceAlpha) {
alpha = (byte) 255;
} else {
alpha = dis.readByte();
}
int ofs = ((j + (i * textureWidth)) * 4);
if (ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN) {
rawData[ofs] = red;
rawData[ofs + 1] = green;
rawData[ofs + 2] = blue;
rawData[ofs + 3] = alpha;
} else {
rawData[ofs] = red;
rawData[ofs + 1] = green;
rawData[ofs + 2] = blue;
rawData[ofs + 3] = alpha;
}
if (alpha == 0) {
rawData[ofs + 2] = 0;
rawData[ofs + 1] = 0;
rawData[ofs] = 0;
}
}
}
}
}
imageStream.close();
if (transparency != null) {
for (int i = 0; i < rawData.length; i += 4) {
boolean match = true;
for (int c = 0; c < 3; c++) {
if (rawData[i + c] != transparency[c]) {
match = false;
}
}
if (match) {
rawData[i + 3] = 0;
}
}
}
// Get a pointer to the image memory
ByteBuffer scratch = createNativeOrderedByteBuffer(rawData.length);
scratch.put(rawData);
int perPixel = imageBitDepth / 8;
if (imageHeight < textureHeight - 1) {
int topOffset = (textureHeight - 1) * (textureWidth * perPixel);
int bottomOffset = (imageHeight - 1) * (textureWidth * perPixel);
for (int x = 0; x < textureWidth * perPixel; x++) {
scratch.put(topOffset + x, scratch.get(x));
scratch.put(bottomOffset + (textureWidth * perPixel) + x,
scratch.get((textureWidth * perPixel) + x));
}
}
if (imageWidth < textureWidth - 1) {
for (int y = 0; y < textureHeight; y++) {
for (int i = 0; i < perPixel; i++) {
scratch.put(((y + 1) * (textureWidth * perPixel)) - perPixel + i,
scratch.get(y * (textureWidth * perPixel) + i));
scratch.put((y * (textureWidth * perPixel)) + (imageWidth * perPixel) + i,
scratch.get((y * (textureWidth * perPixel)) + ((imageWidth - 1) * perPixel) + i));
}
}
}
scratch.flip();
return scratch;
}
// TODO Refactor.
@Nonnull
private ByteBuffer loadImage(
@Nonnull final InputStream imageStream,
final boolean shouldFlipVertically,
final boolean shouldForceAlpha,
@Nullable final int[] transparency,
final boolean forceNonePowerOfTwo,
final boolean modeARGB) throws IOException {
this.shouldFlipVertically = shouldFlipVertically;
this.shouldForceAlpha = shouldForceAlpha;
if (transparency != null) {
this.shouldForceAlpha = true;
}
byte red;
byte green;
byte blue;
byte alpha;
BufferedInputStream bis = new BufferedInputStream(imageStream, 100000);
DataInputStream dis = new DataInputStream(bis);
// Read in the Header
short idLength = (short) dis.read();
dis.skipBytes(11);
/*
short colorMapType = (short) dis.read();
short imageType = (short) dis.read();
short cMapStart = flipEndian(dis.readShort());
short cMapLength = flipEndian(dis.readShort());
short cMapDepth = (short) dis.read();
short xOffset = flipEndian(dis.readShort());
short yOffset = flipEndian(dis.readShort());
*/
imageWidth = flipEndian(dis.readShort());
imageHeight = flipEndian(dis.readShort());
imageBitDepth = (short) dis.read();
if (imageBitDepth == 32) {
this.shouldForceAlpha = false;
}
textureWidth = imageWidth;
textureHeight = imageHeight;
if (forceNonePowerOfTwo) {
textureWidth = get2Fold(imageWidth);
textureHeight = get2Fold(imageHeight);
}
short imageDescriptor = (short) dis.read();
if ((imageDescriptor & 0x0020) == 0) {
this.shouldFlipVertically = !this.shouldFlipVertically;
}
// Skip image ID
if (idLength > 0) {
int skipped = 0;
while (skipped < idLength) {
skipped += bis.skip(idLength - skipped);
}
}
byte[] rawData;
if ((imageBitDepth == 32) || (this.shouldForceAlpha)) {
imageBitDepth = 32;
rawData = new byte[textureWidth * textureHeight * 4];
} else if (imageBitDepth == 24) {
rawData = new byte[textureWidth * textureHeight * 3];
} else {
throw new RuntimeException("Only 24 and 32 bit TGAs are supported");
}
if (imageBitDepth == 24) {
if (this.shouldFlipVertically) {
for (int i = imageHeight - 1; i >= 0; i--) {
for (int j = 0; j < imageWidth; j++) {
blue = dis.readByte();
green = dis.readByte();
red = dis.readByte();
int ofs = ((j + (i * textureWidth)) * 3);
rawData[ofs] = red;
rawData[ofs + 1] = green;
rawData[ofs + 2] = blue;
}
}
} else {
for (int i = 0; i < imageHeight; i++) {
for (int j = 0; j < imageWidth; j++) {
blue = dis.readByte();
green = dis.readByte();
red = dis.readByte();
int ofs = ((j + (i * textureWidth)) * 3);
rawData[ofs] = red;
rawData[ofs + 1] = green;
rawData[ofs + 2] = blue;
}
}
}
} else if (imageBitDepth == 32) {
if (this.shouldFlipVertically) {
for (int i = imageHeight - 1; i >= 0; i--) {
for (int j = 0; j < imageWidth; j++) {
blue = dis.readByte();
green = dis.readByte();
red = dis.readByte();
if (this.shouldForceAlpha) {
alpha = (byte) 255;
} else {
alpha = dis.readByte();
}
int ofs = ((j + (i * textureWidth)) * 4);
rawData[ofs] = red;
rawData[ofs + 1] = green;
rawData[ofs + 2] = blue;
rawData[ofs + 3] = alpha;
if (alpha == 0) {
rawData[ofs + 2] = (byte) 0;
rawData[ofs + 1] = (byte) 0;
rawData[ofs] = (byte) 0;
}
}
}
} else {
for (int i = 0; i < imageHeight; i++) {
for (int j = 0; j < imageWidth; j++) {
blue = dis.readByte();
green = dis.readByte();
red = dis.readByte();
if (this.shouldForceAlpha) {
alpha = (byte) 255;
} else {
alpha = dis.readByte();
}
int ofs = ((j + (i * textureWidth)) * 4);
if (ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN) {
rawData[ofs] = red;
rawData[ofs + 1] = green;
rawData[ofs + 2] = blue;
rawData[ofs + 3] = alpha;
} else {
rawData[ofs] = red;
rawData[ofs + 1] = green;
rawData[ofs + 2] = blue;
rawData[ofs + 3] = alpha;
}
if (alpha == 0) {
rawData[ofs + 2] = 0;
rawData[ofs + 1] = 0;
rawData[ofs] = 0;
}
}
}
}
}
imageStream.close();
if (transparency != null) {
for (int i = 0; i < rawData.length; i += 4) {
boolean match = true;
for (int c = 0; c < 3; c++) {
if (rawData[i + c] != transparency[c]) {
match = false;
}
}
if (match) {
rawData[i + 3] = 0;
}
}
}
// Get a pointer to the image memory
ByteBuffer scratch = createNativeOrderedByteBuffer(rawData.length);
scratch.put(rawData);
int perPixel = imageBitDepth / 8;
if (imageHeight < textureHeight - 1) {
int topOffset = (textureHeight - 1) * (textureWidth * perPixel);
int bottomOffset = (imageHeight - 1) * (textureWidth * perPixel);
for (int x = 0; x < textureWidth * perPixel; x++) {
scratch.put(topOffset + x, scratch.get(x));
scratch.put(bottomOffset + (textureWidth * perPixel) + x, scratch.get((textureWidth * perPixel) + x));
}
}
if (imageWidth < textureWidth - 1) {
for (int y = 0; y < textureHeight; y++) {
for (int i = 0; i < perPixel; i++) {
scratch.put(((y + 1) * (textureWidth * perPixel)) - perPixel + i, scratch.get(y * (textureWidth * perPixel) + i));
scratch.put((y * (textureWidth * perPixel)) + (imageWidth * perPixel) + i, scratch.get((y * (textureWidth * perPixel)) + ((imageWidth - 1) * perPixel) + i));
}
}
}
scratch.flip();
return scratch;
}
/**
* Flip the endian-ness of the short
*
* @param signedShort The short to flip
* @return The flipped short
*/
private short flipEndian(final short signedShort) {
int input = signedShort & 0xFFFF;
return (short) (input << 8 | (input & 0xFF00) >>> 8);
}
@Nonnull
private ByteBuffer createNativeOrderedByteBuffer(final int numBytes) {
return ByteBuffer.allocateDirect(numBytes).order(ByteOrder.nativeOrder());
}
/**
* Get the closest greater power of 2 to the fold number
*
* @param fold The target number
* @return The power of 2
*/
private int get2Fold(final int fold) {
int ret = 2;
while (ret < fold) {
ret *= 2;
}
return ret;
}
}