/******************************************************************************* * Copyright 2011 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package com.gundogstudios.util; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.zip.CRC32; import java.util.zip.DataFormatException; import java.util.zip.Inflater; /** * The PNG imge data source that is pure java reading PNGs * * @author Matthias Mann (original code) */ public class PNGDecoder { /** The valid signature of a PNG */ private static final byte[] SIGNATURE = { (byte) 137, 80, 78, 71, 13, 10, 26, 10 }; /** The header chunk identifer */ private static final int IHDR = 0x49484452; /** The palette chunk identifer */ private static final int PLTE = 0x504C5445; /** The transparency chunk identifier */ private static final int tRNS = 0x74524E53; /** The data chunk identifier */ private static final int IDAT = 0x49444154; /** The end chunk identifier */ // private static final int IEND = 0x49454E44; /** Color type for greyscale images */ private static final byte COLOR_GREYSCALE = 0; /** Color type for true colour images */ private static final byte COLOR_TRUECOLOR = 2; /** Color type for indexed palette images */ private static final byte COLOR_INDEXED = 3; /** Color type for greyscale images with alpha */ // private static final byte COLOR_GREYALPHA = 4; /** Color type for true colour images with alpha */ private static final byte COLOR_TRUEALPHA = 6; /** The stream we're going to read from */ private InputStream input; /** The CRC for the current chunk */ private final CRC32 crc; /** The buffer we'll use as temporary storage */ private final byte[] buffer; /** The length of the current chunk in bytes */ private int chunkLength; /** The ID of the current chunk */ private int chunkType; /** The number of bytes remaining in the current chunk */ private int chunkRemaining; /** The width of the image read */ private int width; /** The height of the image read */ private int height; /** The type of colours in the PNG data */ private int colorType; /** The number of bytes per pixel */ private int bytesPerPixel; /** The palette data that has been read - RGB only */ private byte[] palette; /** The palette data thats be read from alpha channel */ private byte[] paletteA; /** The transparent pixel description */ private byte[] transPixel; /** The bit depth of the image */ private int bitDepth; /** The width of the texture to be generated */ private int texWidth; /** The height of the texture to be generated */ private int texHeight; /** The scratch buffer used to store the image data */ private ByteBuffer scratch; /** * Create a new PNG image data that can read image data from PNG formated files */ public PNGDecoder() { this.crc = new CRC32(); this.buffer = new byte[4096]; } public PNGDecoder(InputStream stream) { this(); try { loadImage(stream); } catch (IOException e) { throw new RuntimeException(e); } } /** * Initialise the PNG data header fields from the input stream * * @param input * The input stream to read from * @throws IOException * Indicates a failure to read appropriate data from the stream */ private void init(InputStream input) throws IOException { this.input = input; int read = input.read(buffer, 0, SIGNATURE.length); if (read != SIGNATURE.length || !checkSignatur(buffer)) { throw new IOException("Not a valid PNG file"); } openChunk(IHDR); readIHDR(); closeChunk(); searchIDAT: for (;;) { openChunk(); switch (chunkType) { case IDAT: break searchIDAT; case PLTE: readPLTE(); break; case tRNS: readtRNS(); break; } closeChunk(); } } /** * @see org.newdawn.slick.opengl.ImageData#getHeights() */ public int getHeight() { return height; } /** * @see org.newdawn.slick.opengl.ImageData#getWidths() */ public int getWidth() { return width; } /** * Check if this PNG has a an alpha channel * * @return True if the PNG has an alpha channel */ public boolean hasAlpha() { return colorType == COLOR_TRUEALPHA || paletteA != null || transPixel != null; } /** * Check if the PNG is RGB formatted * * @return True if the PNG is RGB formatted */ public boolean isRGB() { return colorType == COLOR_TRUEALPHA || colorType == COLOR_TRUECOLOR || colorType == COLOR_INDEXED; } /** * Decode a PNG into a data buffer * * @param buffer * The buffer to read the data into * @param stride * The image stride to read (i.e. the number of bytes to skip each line) * @param flip * True if the PNG should be flipped * @throws IOException * Indicates a failure to read the PNG either invalid data or not enough room in the buffer */ private void decode(ByteBuffer buffer, int stride, boolean flip) throws IOException { final int offset = buffer.position(); byte[] curLine = new byte[width * bytesPerPixel + 1]; byte[] prevLine = new byte[width * bytesPerPixel + 1]; final Inflater inflater = new Inflater(); try { for (int yIndex = 0; yIndex < height; yIndex++) { int y = yIndex; if (flip) { y = height - 1 - yIndex; } readChunkUnzip(inflater, curLine, 0, curLine.length); unfilter(curLine, prevLine); buffer.position(offset + y * stride); switch (colorType) { case COLOR_TRUECOLOR: case COLOR_TRUEALPHA: copy(buffer, curLine); break; case COLOR_INDEXED: copyExpand(buffer, curLine); break; default: throw new UnsupportedOperationException("Not yet implemented"); } byte[] tmp = curLine; curLine = prevLine; prevLine = tmp; } } finally { inflater.end(); } bitDepth = hasAlpha() ? 32 : 24; } /** * Copy some data into the given byte buffer expanding the data based on indexing the palette * * @param buffer * The buffer to write into * @param curLine * The current line of data to copy */ private void copyExpand(ByteBuffer buffer, byte[] curLine) { for (int i = 1; i < curLine.length; i++) { int v = curLine[i] & 255; int index = v * 3; for (int j = 0; j < 3; j++) { buffer.put(palette[index + j]); } if (hasAlpha()) { if (paletteA != null) { buffer.put(paletteA[v]); } else { buffer.put((byte) 255); } } } } /** * Copy the data given directly into the byte buffer (skipping the filter byte); * * @param buffer * The buffer to write into * @param curLine * The current line to copy into the buffer */ private void copy(ByteBuffer buffer, byte[] curLine) { buffer.put(curLine, 1, curLine.length - 1); } /** * Unfilter the data, i.e. convert it back to it's original form * * @param curLine * The line of data just read * @param prevLine * The line before * @throws IOException * Indicates a failure to unfilter the data due to an unknown filter type */ private void unfilter(byte[] curLine, byte[] prevLine) throws IOException { switch (curLine[0]) { case 0: // none break; case 1: unfilterSub(curLine); break; case 2: unfilterUp(curLine, prevLine); break; case 3: unfilterAverage(curLine, prevLine); break; case 4: unfilterPaeth(curLine, prevLine); break; default: throw new IOException("invalide filter type in scanline: " + curLine[0]); } } /** * Sub unfilter {@url http://libpng.nigilist.ru/pub/png/spec/1.2/PNG-Filters.html} * * @param curLine * The line of data to be unfiltered */ private void unfilterSub(byte[] curLine) { final int bpp = this.bytesPerPixel; final int lineSize = width * bpp; for (int i = bpp + 1; i <= lineSize; ++i) { curLine[i] += curLine[i - bpp]; } } /** * Up unfilter {@url http://libpng.nigilist.ru/pub/png/spec/1.2/PNG-Filters.html} * * @param prevLine * The line of data read before the current * @param curLine * The line of data to be unfiltered */ private void unfilterUp(byte[] curLine, byte[] prevLine) { final int bpp = this.bytesPerPixel; final int lineSize = width * bpp; for (int i = 1; i <= lineSize; ++i) { curLine[i] += prevLine[i]; } } /** * Average unfilter {@url http://libpng.nigilist.ru/pub/png/spec/1.2/PNG-Filters.html} * * @param prevLine * The line of data read before the current * @param curLine * The line of data to be unfiltered */ private void unfilterAverage(byte[] curLine, byte[] prevLine) { final int bpp = this.bytesPerPixel; final int lineSize = width * bpp; int i; for (i = 1; i <= bpp; ++i) { curLine[i] += (byte) ((prevLine[i] & 0xFF) >>> 1); } for (; i <= lineSize; ++i) { curLine[i] += (byte) (((prevLine[i] & 0xFF) + (curLine[i - bpp] & 0xFF)) >>> 1); } } /** * Paeth unfilter {@url http://libpng.nigilist.ru/pub/png/spec/1.2/PNG-Filters.html} * * @param prevLine * The line of data read before the current * @param curLine * The line of data to be unfiltered */ private void unfilterPaeth(byte[] curLine, byte[] prevLine) { final int bpp = this.bytesPerPixel; final int lineSize = width * bpp; int i; for (i = 1; i <= bpp; ++i) { curLine[i] += prevLine[i]; } for (; i <= lineSize; ++i) { int a = curLine[i - bpp] & 0xFF; int b = prevLine[i] & 0xFF; int c = prevLine[i - bpp] & 0xFF; int p = a + b - c; int pa = p - a; if (pa < 0) pa = -pa; int pb = p - b; if (pb < 0) pb = -pb; int pc = p - c; if (pc < 0) pc = -pc; if (pa <= pb && pa <= pc) c = a; else if (pb <= pc) c = b; curLine[i] += (byte) c; } } /** * Read the header of the PNG * * @throws IOException * Indicates a failure to read the header */ private void readIHDR() throws IOException { checkChunkLength(13); readChunk(buffer, 0, 13); width = readInt(buffer, 0); height = readInt(buffer, 4); if (buffer[8] != 8) { throw new IOException("Unsupported bit depth"); } colorType = buffer[9] & 255; switch (colorType) { case COLOR_GREYSCALE: bytesPerPixel = 1; break; case COLOR_TRUECOLOR: bytesPerPixel = 3; break; case COLOR_TRUEALPHA: bytesPerPixel = 4; break; case COLOR_INDEXED: bytesPerPixel = 1; break; default: throw new IOException("unsupported color format"); } if (buffer[10] != 0) { throw new IOException("unsupported compression method"); } if (buffer[11] != 0) { throw new IOException("unsupported filtering method"); } if (buffer[12] != 0) { throw new IOException("unsupported interlace method"); } } /** * Read the palette chunk * * @throws IOException * Indicates a failure to fully read the chunk */ private void readPLTE() throws IOException { int paletteEntries = chunkLength / 3; if (paletteEntries < 1 || paletteEntries > 256 || (chunkLength % 3) != 0) { throw new IOException("PLTE chunk has wrong length"); } palette = new byte[paletteEntries * 3]; readChunk(palette, 0, palette.length); } /** * Read the transparency chunk * * @throws IOException * Indicates a failure to fully read the chunk */ private void readtRNS() throws IOException { switch (colorType) { case COLOR_GREYSCALE: checkChunkLength(2); transPixel = new byte[2]; readChunk(transPixel, 0, 2); break; case COLOR_TRUECOLOR: checkChunkLength(6); transPixel = new byte[6]; readChunk(transPixel, 0, 6); break; case COLOR_INDEXED: if (palette == null) { throw new IOException("tRNS chunk without PLTE chunk"); } paletteA = new byte[palette.length / 3]; // initialise default palette values for (int i = 0; i < paletteA.length; i++) { paletteA[i] = (byte) 255; } readChunk(paletteA, 0, paletteA.length); break; default: // just ignore it } } /** * Close the current chunk, skip the remaining data * * @throws IOException * Indicates a failure to read off redundant data */ private void closeChunk() throws IOException { if (chunkRemaining > 0) { // just skip the rest and the CRC input.skip(chunkRemaining + 4); } else { readFully(buffer, 0, 4); int expectedCrc = readInt(buffer, 0); int computedCrc = (int) crc.getValue(); if (computedCrc != expectedCrc) { throw new IOException("Invalid CRC"); } } chunkRemaining = 0; chunkLength = 0; chunkType = 0; } /** * Open the next chunk, determine the type and setup the internal state * * @throws IOException * Indicates a failure to determine chunk information from the stream */ private void openChunk() throws IOException { readFully(buffer, 0, 8); chunkLength = readInt(buffer, 0); chunkType = readInt(buffer, 4); chunkRemaining = chunkLength; crc.reset(); crc.update(buffer, 4, 4); // only chunkType } /** * Open a chunk of an expected type * * @param expected * The expected type of the next chunk * @throws IOException * Indicate a failure to read data or a different chunk on the stream */ private void openChunk(int expected) throws IOException { openChunk(); if (chunkType != expected) { throw new IOException("Expected chunk: " + Integer.toHexString(expected)); } } /** * Check the current chunk has the correct size * * @param expected * The expected size of the chunk * @throws IOException * Indicate an invalid size */ private void checkChunkLength(int expected) throws IOException { if (chunkLength != expected) { throw new IOException("Chunk has wrong size"); } } /** * Read some data from the current chunk * * @param buffer * The buffer to read into * @param offset * The offset into the buffer to read into * @param length * The amount of data to read * @return The number of bytes read from the chunk * @throws IOException * Indicate a failure to read the appropriate data from the chunk */ private int readChunk(byte[] buffer, int offset, int length) throws IOException { if (length > chunkRemaining) { length = chunkRemaining; } readFully(buffer, offset, length); crc.update(buffer, offset, length); chunkRemaining -= length; return length; } /** * Refill the inflating stream with data from the stream * * @param inflater * The inflater to fill * @throws IOException * Indicates there is no more data left or invalid data has been found on the stream. */ private void refillInflater(Inflater inflater) throws IOException { while (chunkRemaining == 0) { closeChunk(); openChunk(IDAT); } int read = readChunk(buffer, 0, buffer.length); inflater.setInput(buffer, 0, read); } /** * Read a chunk from the inflater * * @param inflater * The inflater to read the data from * @param buffer * The buffer to write into * @param offset * The offset into the buffer at which to start writing * @param length * The number of bytes to read * @throws IOException * Indicates a failure to read the complete chunk */ private void readChunkUnzip(Inflater inflater, byte[] buffer, int offset, int length) throws IOException { try { do { int read = inflater.inflate(buffer, offset, length); if (read <= 0) { if (inflater.finished()) { throw new EOFException(); } if (inflater.needsInput()) { refillInflater(inflater); } else { throw new IOException("Can't inflate " + length + " bytes"); } } else { offset += read; length -= read; } } while (length > 0); } catch (DataFormatException ex) { IOException io = new IOException("inflate error"); io.initCause(ex); throw io; } } /** * Read a complete buffer of data from the input stream * * @param buffer * The buffer to read into * @param offset * The offset to start copying into * @param length * The length of bytes to read * @throws IOException * Indicates a failure to access the data */ private void readFully(byte[] buffer, int offset, int length) throws IOException { do { int read = input.read(buffer, offset, length); if (read < 0) { throw new EOFException(); } offset += read; length -= read; } while (length > 0); } /** * Read an int from a buffer * * @param buffer * The buffer to read from * @param offset * The offset into the buffer to read from * @return The int read interpreted in big endian */ private int readInt(byte[] buffer, int offset) { return ((buffer[offset]) << 24) | ((buffer[offset + 1] & 255) << 16) | ((buffer[offset + 2] & 255) << 8) | ((buffer[offset + 3] & 255)); } /** * Check the signature of the PNG to confirm it's a PNG * * @param buffer * The buffer to read from * @return True if the PNG signature is correct */ private boolean checkSignatur(byte[] buffer) { for (int i = 0; i < SIGNATURE.length; i++) { if (buffer[i] != SIGNATURE[i]) { return false; } } return true; } /** * @see org.newdawn.slick.opengl.ImageData#getDepth() */ public int getDepth() { return bitDepth; } /** * @see org.newdawn.slick.opengl.ImageData#getImageBufferData() */ public ByteBuffer getImageBufferData() { return scratch; } /** * @see org.newdawn.slick.opengl.ImageData#getTexHeight() */ public int getTexHeight() { return texHeight; } /** * @see org.newdawn.slick.opengl.ImageData#getTexWidth() */ public int getTexWidth() { return texWidth; } /** * @see org.newdawn.slick.opengl.LoadableImageData#loadImage(java.io.InputStream) */ public ByteBuffer loadImage(InputStream fis) throws IOException { return loadImage(fis, false, null); } /** * @see org.newdawn.slick.opengl.LoadableImageData#loadImage(java.io.InputStream, boolean, int[]) */ public ByteBuffer loadImage(InputStream fis, boolean flipped, int[] transparent) throws IOException { return loadImage(fis, flipped, false, transparent); } /** * @see org.newdawn.slick.opengl.LoadableImageData#loadImage(java.io.InputStream, boolean, boolean, int[]) */ public ByteBuffer loadImage(InputStream fis, boolean flipped, boolean forceAlpha, int[] transparent) throws IOException { if (transparent != null) { forceAlpha = true; } init(fis); if (!isRGB()) { throw new IOException("Only RGB formatted images are supported by the PNGLoader"); } texWidth = get2Fold(width); texHeight = get2Fold(height); int perPixel = hasAlpha() ? 4 : 3; // Get a pointer to the image memory scratch = ByteBuffer.allocateDirect(texWidth * texHeight * perPixel).order(ByteOrder.nativeOrder()); decode(scratch, texWidth * perPixel, flipped); if (height < texHeight - 1) { int topOffset = (texHeight - 1) * (texWidth * perPixel); int bottomOffset = (height - 1) * (texWidth * perPixel); for (int x = 0; x < texWidth; x++) { for (int i = 0; i < perPixel; i++) { scratch.put(topOffset + x + i, scratch.get(x + i)); scratch.put(bottomOffset + (texWidth * perPixel) + x + i, scratch.get(bottomOffset + x + i)); } } } if (width < texWidth - 1) { for (int y = 0; y < texHeight; y++) { for (int i = 0; i < perPixel; i++) { scratch.put(((y + 1) * (texWidth * perPixel)) - perPixel + i, scratch.get(y * (texWidth * perPixel) + i)); scratch.put((y * (texWidth * perPixel)) + (width * perPixel) + i, scratch.get((y * (texWidth * perPixel)) + ((width - 1) * perPixel) + i)); } } } if (!hasAlpha() && forceAlpha) { ByteBuffer temp = ByteBuffer.allocateDirect(texWidth * texHeight * 4).order(ByteOrder.nativeOrder()); for (int x = 0; x < texWidth; x++) { for (int y = 0; y < texHeight; y++) { int srcOffset = (y * 3) + (x * texHeight * 3); int dstOffset = (y * 4) + (x * texHeight * 4); temp.put(dstOffset, scratch.get(srcOffset)); temp.put(dstOffset + 1, scratch.get(srcOffset + 1)); temp.put(dstOffset + 2, scratch.get(srcOffset + 2)); temp.put(dstOffset + 3, (byte) 255); } } colorType = COLOR_TRUEALPHA; bitDepth = 32; scratch = temp; } if (transparent != null) { for (int i = 0; i < texWidth * texHeight * 4; i += 4) { boolean match = true; for (int c = 0; c < 3; c++) { if (toInt(scratch.get(i + c)) != transparent[c]) { match = false; } } if (match) { scratch.put(i + 3, (byte) 0); } } } scratch.position(0); return scratch; } /** * Safe convert byte to int * * @param b * The byte to convert * @return The converted byte */ private int toInt(byte b) { if (b < 0) { return 256 + b; } return b; } /** * Get the closest greater power of 2 to the fold number * * @param fold * The target number * @return The power of 2 */ private int get2Fold(int fold) { int ret = 2; while (ret < fold) { ret *= 2; } return ret; } /** * @see org.newdawn.slick.opengl.LoadableImageData#configureEdging(boolean) */ public void configureEdging(boolean edging) { } @Override public String toString() { return "PNGImageData [width=" + width + ", height=" + height + ", colorType=" + colorType + ", bytesPerPixel=" + bytesPerPixel + ", bitDepth=" + bitDepth + ", texWidth=" + texWidth + ", texHeight=" + texHeight + "]"; } }