/* * Copyright (c) 2009-2012 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 * * * @param in * InputStream of an uncompressed 24b RGB or 32b RGBA TGA * @param flip * Flip the image vertically * @return <code>Image</code> object that contains the * image, either as a RGB888 or RGBA8888 * @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) { dis.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]; dis.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.blue; rawData[rawDataIndex++] = entry.green; rawData[rawDataIndex++] = entry.red; 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.blue; rawData[rawDataIndex++] = entry.green; rawData[rawDataIndex++] = entry.red; 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("Monochrome and RLE colormapped images are 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; } } }