blob: bbd51d6414eb8c2dd52ae8c2804ded0a3bcd7ad8 [file] [log] [blame]
/*
* Copyright (c) 2009-2010 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.texture.plugins;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetLoader;
import com.jme3.asset.TextureKey;
import com.jme3.math.FastMath;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.util.BufferUtils;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* <code>TextureManager</code> provides static methods for building a
* <code>Texture</code> object. Typically, the information supplied is the
* filename and the texture properties.
*
* @author Mark Powell
* @author Joshua Slack - cleaned, commented, added ability to read 16bit true color and color-mapped TGAs.
* @author Kirill Vainer - ported to jME3
* @version $Id: TGALoader.java 4131 2009-03-19 20:15:28Z blaine.dev $
*/
public final class TGALoader implements AssetLoader {
// 0 - no image data in file
public static final int TYPE_NO_IMAGE = 0;
// 1 - uncompressed, color-mapped image
public static final int TYPE_COLORMAPPED = 1;
// 2 - uncompressed, true-color image
public static final int TYPE_TRUECOLOR = 2;
// 3 - uncompressed, black and white image
public static final int TYPE_BLACKANDWHITE = 3;
// 9 - run-length encoded, color-mapped image
public static final int TYPE_COLORMAPPED_RLE = 9;
// 10 - run-length encoded, true-color image
public static final int TYPE_TRUECOLOR_RLE = 10;
// 11 - run-length encoded, black and white image
public static final int TYPE_BLACKANDWHITE_RLE = 11;
public Object load(AssetInfo info) throws IOException{
if (!(info.getKey() instanceof TextureKey))
throw new IllegalArgumentException("Texture assets must be loaded using a TextureKey");
boolean flip = ((TextureKey)info.getKey()).isFlipY();
InputStream in = null;
try {
in = info.openStream();
Image img = load(in, flip);
return img;
} finally {
if (in != null){
in.close();
}
}
}
/**
* <code>loadImage</code> is a manual image loader which is entirely
* independent of AWT. OUT: RGB888 or RGBA8888 Image object
*
* @return <code>Image</code> object that contains the
* image, either as a RGB888 or RGBA8888
* @param flip
* Flip the image vertically
* @param exp32
* Add a dummy Alpha channel to 24b RGB image.
* @param fis
* InputStream of an uncompressed 24b RGB or 32b RGBA TGA
* @throws java.io.IOException
*/
public static Image load(InputStream in, boolean flip) throws IOException {
boolean flipH = false;
// open a stream to the file
DataInputStream dis = new DataInputStream(new BufferedInputStream(in));
// ---------- Start Reading the TGA header ---------- //
// length of the image id (1 byte)
int idLength = dis.readUnsignedByte();
// Type of color map (if any) included with the image
// 0 - no color map data is included
// 1 - a color map is included
int colorMapType = dis.readUnsignedByte();
// Type of image being read:
int imageType = dis.readUnsignedByte();
// Read Color Map Specification (5 bytes)
// Index of first color map entry (if we want to use it, uncomment and remove extra read.)
// short cMapStart = flipEndian(dis.readShort());
dis.readShort();
// number of entries in the color map
short cMapLength = flipEndian(dis.readShort());
// number of bits per color map entry
int cMapDepth = dis.readUnsignedByte();
// Read Image Specification (10 bytes)
// horizontal coordinate of lower left corner of image. (if we want to use it, uncomment and remove extra read.)
// int xOffset = flipEndian(dis.readShort());
dis.readShort();
// vertical coordinate of lower left corner of image. (if we want to use it, uncomment and remove extra read.)
// int yOffset = flipEndian(dis.readShort());
dis.readShort();
// width of image - in pixels
int width = flipEndian(dis.readShort());
// height of image - in pixels
int height = flipEndian(dis.readShort());
// bits per pixel in image.
int pixelDepth = dis.readUnsignedByte();
int imageDescriptor = dis.readUnsignedByte();
if ((imageDescriptor & 32) != 0) // bit 5 : if 1, flip top/bottom ordering
flip = !flip;
if ((imageDescriptor & 16) != 0) // bit 4 : if 1, flip left/right ordering
flipH = !flipH;
// ---------- Done Reading the TGA header ---------- //
// Skip image ID
if (idLength > 0)
in.skip(idLength);
ColorMapEntry[] cMapEntries = null;
if (colorMapType != 0) {
// read the color map.
int bytesInColorMap = (cMapDepth * cMapLength) >> 3;
int bitsPerColor = Math.min(cMapDepth/3 , 8);
byte[] cMapData = new byte[bytesInColorMap];
in.read(cMapData);
// Only go to the trouble of constructing the color map
// table if this is declared a color mapped image.
if (imageType == TYPE_COLORMAPPED || imageType == TYPE_COLORMAPPED_RLE) {
cMapEntries = new ColorMapEntry[cMapLength];
int alphaSize = cMapDepth - (3*bitsPerColor);
float scalar = 255f / (FastMath.pow(2, bitsPerColor) - 1);
float alphaScalar = 255f / (FastMath.pow(2, alphaSize) - 1);
for (int i = 0; i < cMapLength; i++) {
ColorMapEntry entry = new ColorMapEntry();
int offset = cMapDepth * i;
entry.red = (byte)(int)(getBitsAsByte(cMapData, offset, bitsPerColor) * scalar);
entry.green = (byte)(int)(getBitsAsByte(cMapData, offset+bitsPerColor, bitsPerColor) * scalar);
entry.blue = (byte)(int)(getBitsAsByte(cMapData, offset+(2*bitsPerColor), bitsPerColor) * scalar);
if (alphaSize <= 0)
entry.alpha = (byte)255;
else
entry.alpha = (byte)(int)(getBitsAsByte(cMapData, offset+(3*bitsPerColor), alphaSize) * alphaScalar);
cMapEntries[i] = entry;
}
}
}
// Allocate image data array
Format format;
byte[] rawData = null;
int dl;
if (pixelDepth == 32) {
rawData = new byte[width * height * 4];
dl = 4;
} else {
rawData = new byte[width * height * 3];
dl = 3;
}
int rawDataIndex = 0;
if (imageType == TYPE_TRUECOLOR) {
byte red = 0;
byte green = 0;
byte blue = 0;
byte alpha = 0;
// Faster than doing a 16-or-24-or-32 check on each individual pixel,
// just make a seperate loop for each.
if (pixelDepth == 16) {
byte[] data = new byte[2];
float scalar = 255f/31f;
for (int i = 0; i <= (height - 1); i++) {
if (!flip)
rawDataIndex = (height - 1 - i) * width * dl;
for (int j = 0; j < width; j++) {
data[1] = dis.readByte();
data[0] = dis.readByte();
rawData[rawDataIndex++] = (byte)(int)(getBitsAsByte(data, 1, 5) * scalar);
rawData[rawDataIndex++] = (byte)(int)(getBitsAsByte(data, 6, 5) * scalar);
rawData[rawDataIndex++] = (byte)(int)(getBitsAsByte(data, 11, 5) * scalar);
if (dl == 4) {
// create an alpha channel
alpha = getBitsAsByte(data, 0, 1);
if (alpha == 1) alpha = (byte)255;
rawData[rawDataIndex++] = alpha;
}
}
}
format = dl == 4 ? Format.RGBA8 : Format.RGB8;
} else if (pixelDepth == 24){
for (int y = 0; y < height; y++) {
if (!flip)
rawDataIndex = (height - 1 - y) * width * dl;
else
rawDataIndex = y * width * dl;
dis.readFully(rawData, rawDataIndex, width * dl);
// for (int x = 0; x < width; x++) {
//read scanline
// blue = dis.readByte();
// green = dis.readByte();
// red = dis.readByte();
// rawData[rawDataIndex++] = red;
// rawData[rawDataIndex++] = green;
// rawData[rawDataIndex++] = blue;
// }
}
format = Format.BGR8;
} else if (pixelDepth == 32){
for (int i = 0; i <= (height - 1); i++) {
if (!flip)
rawDataIndex = (height - 1 - i) * width * dl;
for (int j = 0; j < width; j++) {
blue = dis.readByte();
green = dis.readByte();
red = dis.readByte();
alpha = dis.readByte();
rawData[rawDataIndex++] = red;
rawData[rawDataIndex++] = green;
rawData[rawDataIndex++] = blue;
rawData[rawDataIndex++] = alpha;
}
}
format = Format.RGBA8;
}else{
throw new IOException("Unsupported TGA true color depth: "+pixelDepth);
}
} else if( imageType == TYPE_TRUECOLOR_RLE ) {
byte red = 0;
byte green = 0;
byte blue = 0;
byte alpha = 0;
// Faster than doing a 16-or-24-or-32 check on each individual pixel,
// just make a seperate loop for each.
if( pixelDepth == 32 ){
for( int i = 0; i <= ( height - 1 ); ++i ){
if( !flip ){
rawDataIndex = ( height - 1 - i ) * width * dl;
}
for( int j = 0; j < width; ++j ){
// Get the number of pixels the next chunk covers (either packed or unpacked)
int count = dis.readByte();
if( ( count & 0x80 ) != 0 ){
// Its an RLE packed block - use the following 1 pixel for the next <count> pixels
count &= 0x07f;
j += count;
blue = dis.readByte();
green = dis.readByte();
red = dis.readByte();
alpha = dis.readByte();
while( count-- >= 0 ){
rawData[rawDataIndex++] = red;
rawData[rawDataIndex++] = green;
rawData[rawDataIndex++] = blue;
rawData[rawDataIndex++] = alpha;
}
} else{
// Its not RLE packed, but the next <count> pixels are raw.
j += count;
while( count-- >= 0 ){
blue = dis.readByte();
green = dis.readByte();
red = dis.readByte();
alpha = dis.readByte();
rawData[rawDataIndex++] = red;
rawData[rawDataIndex++] = green;
rawData[rawDataIndex++] = blue;
rawData[rawDataIndex++] = alpha;
}
}
}
}
format = Format.RGBA8;
} else if( pixelDepth == 24 ){
for( int i = 0; i <= ( height - 1 ); i++ ){
if( !flip ){
rawDataIndex = ( height - 1 - i ) * width * dl;
}
for( int j = 0; j < width; ++j ){
// Get the number of pixels the next chunk covers (either packed or unpacked)
int count = dis.readByte();
if( ( count & 0x80 ) != 0 ){
// Its an RLE packed block - use the following 1 pixel for the next <count> pixels
count &= 0x07f;
j += count;
blue = dis.readByte();
green = dis.readByte();
red = dis.readByte();
while( count-- >= 0 ){
rawData[rawDataIndex++] = red;
rawData[rawDataIndex++] = green;
rawData[rawDataIndex++] = blue;
}
} else{
// Its not RLE packed, but the next <count> pixels are raw.
j += count;
while( count-- >= 0 ){
blue = dis.readByte();
green = dis.readByte();
red = dis.readByte();
rawData[rawDataIndex++] = red;
rawData[rawDataIndex++] = green;
rawData[rawDataIndex++] = blue;
}
}
}
}
format = Format.RGB8;
} else if( pixelDepth == 16 ){
byte[] data = new byte[ 2 ];
float scalar = 255f / 31f;
for( int i = 0; i <= ( height - 1 ); i++ ){
if( !flip ){
rawDataIndex = ( height - 1 - i ) * width * dl;
}
for( int j = 0; j < width; j++ ){
// Get the number of pixels the next chunk covers (either packed or unpacked)
int count = dis.readByte();
if( ( count & 0x80 ) != 0 ){
// Its an RLE packed block - use the following 1 pixel for the next <count> pixels
count &= 0x07f;
j += count;
data[1] = dis.readByte();
data[0] = dis.readByte();
blue = (byte) (int) ( getBitsAsByte( data, 1, 5 ) * scalar );
green = (byte) (int) ( getBitsAsByte( data, 6, 5 ) * scalar );
red = (byte) (int) ( getBitsAsByte( data, 11, 5 ) * scalar );
while( count-- >= 0 ){
rawData[rawDataIndex++] = red;
rawData[rawDataIndex++] = green;
rawData[rawDataIndex++] = blue;
}
} else{
// Its not RLE packed, but the next <count> pixels are raw.
j += count;
while( count-- >= 0 ){
data[1] = dis.readByte();
data[0] = dis.readByte();
blue = (byte) (int) ( getBitsAsByte( data, 1, 5 ) * scalar );
green = (byte) (int) ( getBitsAsByte( data, 6, 5 ) * scalar );
red = (byte) (int) ( getBitsAsByte( data, 11, 5 ) * scalar );
rawData[rawDataIndex++] = red;
rawData[rawDataIndex++] = green;
rawData[rawDataIndex++] = blue;
}
}
}
}
format = Format.RGB8;
} else{
throw new IOException( "Unsupported TGA true color depth: " + pixelDepth );
}
} else if( imageType == TYPE_COLORMAPPED ){
int bytesPerIndex = pixelDepth / 8;
if (bytesPerIndex == 1) {
for (int i = 0; i <= (height - 1); i++) {
if (!flip)
rawDataIndex = (height - 1 - i) * width * dl;
for (int j = 0; j < width; j++) {
int index = dis.readUnsignedByte();
if (index >= cMapEntries.length || index < 0)
throw new IOException("TGA: Invalid color map entry referenced: "+index);
ColorMapEntry entry = cMapEntries[index];
rawData[rawDataIndex++] = entry.red;
rawData[rawDataIndex++] = entry.green;
rawData[rawDataIndex++] = entry.blue;
if (dl == 4) {
rawData[rawDataIndex++] = entry.alpha;
}
}
}
} else if (bytesPerIndex == 2) {
for (int i = 0; i <= (height - 1); i++) {
if (!flip)
rawDataIndex = (height - 1 - i) * width * dl;
for (int j = 0; j < width; j++) {
int index = flipEndian(dis.readShort());
if (index >= cMapEntries.length || index < 0)
throw new IOException("TGA: Invalid color map entry referenced: "+index);
ColorMapEntry entry = cMapEntries[index];
rawData[rawDataIndex++] = entry.red;
rawData[rawDataIndex++] = entry.green;
rawData[rawDataIndex++] = entry.blue;
if (dl == 4) {
rawData[rawDataIndex++] = entry.alpha;
}
}
}
} else {
throw new IOException("TGA: unknown colormap indexing size used: "+bytesPerIndex);
}
format = dl == 4 ? Format.RGBA8 : Format.RGB8;
} else {
throw new IOException("Grayscale TGA not supported");
}
in.close();
// Get a pointer to the image memory
ByteBuffer scratch = BufferUtils.createByteBuffer(rawData.length);
scratch.clear();
scratch.put(rawData);
scratch.rewind();
// Create the Image object
Image textureImage = new Image();
textureImage.setFormat(format);
textureImage.setWidth(width);
textureImage.setHeight(height);
textureImage.setData(scratch);
return textureImage;
}
private static byte getBitsAsByte(byte[] data, int offset, int length) {
int offsetBytes = offset / 8;
int indexBits = offset % 8;
int rVal = 0;
// start at data[offsetBytes]... spill into next byte as needed.
for (int i = length; --i >=0;) {
byte b = data[offsetBytes];
int test = indexBits == 7 ? 1 : 2 << (6-indexBits);
if ((b & test) != 0) {
if (i == 0)
rVal++;
else
rVal += (2 << i-1);
}
indexBits++;
if (indexBits == 8) {
indexBits = 0;
offsetBytes++;
}
}
return (byte)rVal;
}
/**
* <code>flipEndian</code> is used to flip the endian bit of the header
* file.
*
* @param signedShort
* the bit to flip.
* @return the flipped bit.
*/
private static short flipEndian(short signedShort) {
int input = signedShort & 0xFFFF;
return (short) (input << 8 | (input & 0xFF00) >>> 8);
}
static class ColorMapEntry {
byte red, green, blue, alpha;
@Override
public String toString() {
return "entry: "+red+","+green+","+blue+","+alpha;
}
}
}