Path: blob/master/SLICK_HOME/src/org/newdawn/slick/opengl/PNGImageData.java
1461 views
package org.newdawn.slick.opengl;12import java.io.EOFException;3import java.io.IOException;4import java.io.InputStream;5import java.nio.ByteBuffer;6import java.util.zip.CRC32;7import java.util.zip.DataFormatException;8import java.util.zip.Inflater;910import org.lwjgl.BufferUtils;1112/**13* The PNG imge data source that is pure java reading PNGs14*15* @author Matthias Mann (original code)16*/17public class PNGImageData implements LoadableImageData {18/** The valid signature of a PNG */19private static final byte[] SIGNATURE = {(byte)137, 80, 78, 71, 13, 10, 26, 10};2021/** The header chunk identifer */22private static final int IHDR = 0x49484452;23/** The palette chunk identifer */24private static final int PLTE = 0x504C5445;25/** The transparency chunk identifier */26private static final int tRNS = 0x74524E53;27/** The data chunk identifier */28private static final int IDAT = 0x49444154;29/** The end chunk identifier */30private static final int IEND = 0x49454E44;3132/** Color type for greyscale images */33private static final byte COLOR_GREYSCALE = 0;34/** Color type for true colour images */35private static final byte COLOR_TRUECOLOR = 2;36/** Color type for indexed palette images */37private static final byte COLOR_INDEXED = 3;38/** Color type for greyscale images with alpha */39private static final byte COLOR_GREYALPHA = 4;40/** Color type for true colour images with alpha */41private static final byte COLOR_TRUEALPHA = 6;4243/** The stream we're going to read from */44private InputStream input;45/** The CRC for the current chunk */46private final CRC32 crc;47/** The buffer we'll use as temporary storage */48private final byte[] buffer;4950/** The length of the current chunk in bytes */51private int chunkLength;52/** The ID of the current chunk */53private int chunkType;54/** The number of bytes remaining in the current chunk */55private int chunkRemaining;5657/** The width of the image read */58private int width;59/** The height of the image read */60private int height;61/** The type of colours in the PNG data */62private int colorType;63/** The number of bytes per pixel */64private int bytesPerPixel;65/** The palette data that has been read - RGB only */66private byte[] palette;67/** The palette data thats be read from alpha channel */68private byte[] paletteA;69/** The transparent pixel description */70private byte[] transPixel;7172/** The bit depth of the image */73private int bitDepth;74/** The width of the texture to be generated */75private int texWidth;76/** The height of the texture to be generated */77private int texHeight;7879/** The scratch buffer used to store the image data */80private ByteBuffer scratch;8182/**83* Create a new PNG image data that can read image data from PNG formated files84*/85public PNGImageData() {86this.crc = new CRC32();87this.buffer = new byte[4096];88}8990/**91* Initialise the PNG data header fields from the input stream92*93* @param input The input stream to read from94* @throws IOException Indicates a failure to read appropriate data from the stream95*/96private void init(InputStream input) throws IOException {97this.input = input;9899int read = input.read(buffer, 0, SIGNATURE.length);100if(read != SIGNATURE.length || !checkSignatur(buffer)) {101throw new IOException("Not a valid PNG file");102}103104openChunk(IHDR);105readIHDR();106closeChunk();107108searchIDAT: for(;;) {109openChunk();110switch (chunkType) {111case IDAT:112break searchIDAT;113case PLTE:114readPLTE();115break;116case tRNS:117readtRNS();118break;119}120closeChunk();121}122}123124/**125* @see org.newdawn.slick.opengl.ImageData#getHeight()126*/127public int getHeight() {128return height;129}130131/**132* @see org.newdawn.slick.opengl.ImageData#getWidth()133*/134public int getWidth() {135return width;136}137138/**139* Check if this PNG has a an alpha channel140*141* @return True if the PNG has an alpha channel142*/143public boolean hasAlpha() {144return colorType == COLOR_TRUEALPHA ||145paletteA != null || transPixel != null;146}147148/**149* Check if the PNG is RGB formatted150*151* @return True if the PNG is RGB formatted152*/153public boolean isRGB() {154return colorType == COLOR_TRUEALPHA ||155colorType == COLOR_TRUECOLOR ||156colorType == COLOR_INDEXED;157}158159/**160* Decode a PNG into a data buffer161*162* @param buffer The buffer to read the data into163* @param stride The image stride to read (i.e. the number of bytes to skip each line)164* @param flip True if the PNG should be flipped165* @throws IOException Indicates a failure to read the PNG either invalid data or166* not enough room in the buffer167*/168private void decode(ByteBuffer buffer, int stride, boolean flip) throws IOException {169final int offset = buffer.position();170byte[] curLine = new byte[width*bytesPerPixel+1];171byte[] prevLine = new byte[width*bytesPerPixel+1];172173final Inflater inflater = new Inflater();174try {175for(int yIndex=0 ; yIndex<height ; yIndex++) {176int y = yIndex;177if (flip) {178y = height - 1 - yIndex;179}180181readChunkUnzip(inflater, curLine, 0, curLine.length);182unfilter(curLine, prevLine);183184buffer.position(offset + y*stride);185186switch (colorType) {187case COLOR_TRUECOLOR:188case COLOR_TRUEALPHA:189copy(buffer, curLine);190break;191case COLOR_INDEXED:192copyExpand(buffer, curLine);193break;194default:195throw new UnsupportedOperationException("Not yet implemented");196}197198byte[] tmp = curLine;199curLine = prevLine;200prevLine = tmp;201}202} finally {203inflater.end();204}205206bitDepth = hasAlpha() ? 32 : 24;207}208209/**210* Copy some data into the given byte buffer expanding the211* data based on indexing the palette212*213* @param buffer The buffer to write into214* @param curLine The current line of data to copy215*/216private void copyExpand(ByteBuffer buffer, byte[] curLine) {217for (int i=1;i<curLine.length;i++) {218int v = curLine[i] & 255;219220int index = v * 3;221for (int j=0;j<3;j++) {222buffer.put(palette[index+j]);223}224225if (hasAlpha()) {226if (paletteA != null) {227buffer.put(paletteA[v]);228} else {229buffer.put((byte) 255);230}231}232}233}234235/**236* Copy the data given directly into the byte buffer (skipping237* the filter byte);238*239* @param buffer The buffer to write into240* @param curLine The current line to copy into the buffer241*/242private void copy(ByteBuffer buffer, byte[] curLine) {243buffer.put(curLine, 1, curLine.length-1);244}245246/**247* Unfilter the data, i.e. convert it back to it's original form248*249* @param curLine The line of data just read250* @param prevLine The line before251* @throws IOException Indicates a failure to unfilter the data due to an unknown252* filter type253*/254private void unfilter(byte[] curLine, byte[] prevLine) throws IOException {255switch (curLine[0]) {256case 0: // none257break;258case 1:259unfilterSub(curLine);260break;261case 2:262unfilterUp(curLine, prevLine);263break;264case 3:265unfilterAverage(curLine, prevLine);266break;267case 4:268unfilterPaeth(curLine, prevLine);269break;270default:271throw new IOException("invalide filter type in scanline: " + curLine[0]);272}273}274275/**276* Sub unfilter277* {@url http://libpng.nigilist.ru/pub/png/spec/1.2/PNG-Filters.html}278*279* @param curLine The line of data to be unfiltered280*/281private void unfilterSub(byte[] curLine) {282final int bpp = this.bytesPerPixel;283final int lineSize = width*bpp;284285for(int i=bpp+1 ; i<=lineSize ; ++i) {286curLine[i] += curLine[i-bpp];287}288}289290/**291* Up unfilter292* {@url http://libpng.nigilist.ru/pub/png/spec/1.2/PNG-Filters.html}293*294* @param prevLine The line of data read before the current295* @param curLine The line of data to be unfiltered296*/297private void unfilterUp(byte[] curLine, byte[] prevLine) {298final int bpp = this.bytesPerPixel;299final int lineSize = width*bpp;300301for(int i=1 ; i<=lineSize ; ++i) {302curLine[i] += prevLine[i];303}304}305306/**307* Average unfilter308* {@url http://libpng.nigilist.ru/pub/png/spec/1.2/PNG-Filters.html}309*310* @param prevLine The line of data read before the current311* @param curLine The line of data to be unfiltered312*/313private void unfilterAverage(byte[] curLine, byte[] prevLine) {314final int bpp = this.bytesPerPixel;315final int lineSize = width*bpp;316317int i;318for(i=1 ; i<=bpp ; ++i) {319curLine[i] += (byte)((prevLine[i] & 0xFF) >>> 1);320}321for(; i<=lineSize ; ++i) {322curLine[i] += (byte)(((prevLine[i] & 0xFF) + (curLine[i - bpp] & 0xFF)) >>> 1);323}324}325326/**327* Paeth unfilter328* {@url http://libpng.nigilist.ru/pub/png/spec/1.2/PNG-Filters.html}329*330* @param prevLine The line of data read before the current331* @param curLine The line of data to be unfiltered332*/333private void unfilterPaeth(byte[] curLine, byte[] prevLine) {334final int bpp = this.bytesPerPixel;335final int lineSize = width*bpp;336337int i;338for(i=1 ; i<=bpp ; ++i) {339curLine[i] += prevLine[i];340}341for(; i<=lineSize ; ++i) {342int a = curLine[i - bpp] & 255;343int b = prevLine[i] & 255;344int c = prevLine[i - bpp] & 255;345int p = a + b - c;346int pa = p - a; if(pa < 0) pa = -pa;347int pb = p - b; if(pb < 0) pb = -pb;348int pc = p - c; if(pc < 0) pc = -pc;349if(pa<=pb && pa<=pc)350c = a;351else if(pb<=pc)352c = b;353curLine[i] += (byte)c;354}355}356357/**358* Read the header of the PNG359*360* @throws IOException Indicates a failure to read the header361*/362private void readIHDR() throws IOException {363checkChunkLength(13);364readChunk(buffer, 0, 13);365width = readInt(buffer, 0);366height = readInt(buffer, 4);367368if(buffer[8] != 8) {369throw new IOException("Unsupported bit depth");370}371372colorType = buffer[9] & 255;373switch (colorType) {374case COLOR_GREYSCALE:375bytesPerPixel = 1;376break;377case COLOR_TRUECOLOR:378bytesPerPixel = 3;379break;380case COLOR_TRUEALPHA:381bytesPerPixel = 4;382break;383case COLOR_INDEXED:384bytesPerPixel = 1;385break;386default:387throw new IOException("unsupported color format");388}389390if(buffer[10] != 0) {391throw new IOException("unsupported compression method");392}393if(buffer[11] != 0) {394throw new IOException("unsupported filtering method");395}396if(buffer[12] != 0) {397throw new IOException("unsupported interlace method");398}399}400401/**402* Read the palette chunk403*404* @throws IOException Indicates a failure to fully read the chunk405*/406private void readPLTE() throws IOException {407int paletteEntries = chunkLength / 3;408if(paletteEntries < 1 || paletteEntries > 256 || (chunkLength % 3) != 0) {409throw new IOException("PLTE chunk has wrong length");410}411412palette = new byte[paletteEntries*3];413readChunk(palette, 0, palette.length);414}415416/**417* Read the transparency chunk418*419* @throws IOException Indicates a failure to fully read the chunk420*/421private void readtRNS() throws IOException {422switch (colorType) {423case COLOR_GREYSCALE:424checkChunkLength(2);425transPixel = new byte[2];426readChunk(transPixel, 0, 2);427break;428case COLOR_TRUECOLOR:429checkChunkLength(6);430transPixel = new byte[6];431readChunk(transPixel, 0, 6);432break;433case COLOR_INDEXED:434if(palette == null) {435throw new IOException("tRNS chunk without PLTE chunk");436}437paletteA = new byte[palette.length/3];438// initialise default palette values439for (int i=0;i<paletteA.length;i++) {440paletteA[i] = (byte) 255;441}442readChunk(paletteA, 0, paletteA.length);443break;444default:445// just ignore it446}447}448449/**450* Close the current chunk, skip the remaining data451*452* @throws IOException Indicates a failure to read off redundant data453*/454private void closeChunk() throws IOException {455if(chunkRemaining > 0) {456// just skip the rest and the CRC457input.skip(chunkRemaining+4);458} else {459readFully(buffer, 0, 4);460int expectedCrc = readInt(buffer, 0);461int computedCrc = (int)crc.getValue();462if(computedCrc != expectedCrc) {463throw new IOException("Invalid CRC");464}465}466chunkRemaining = 0;467chunkLength = 0;468chunkType = 0;469}470471/**472* Open the next chunk, determine the type and setup the internal state473*474* @throws IOException Indicates a failure to determine chunk information from the stream475*/476private void openChunk() throws IOException {477readFully(buffer, 0, 8);478chunkLength = readInt(buffer, 0);479chunkType = readInt(buffer, 4);480chunkRemaining = chunkLength;481crc.reset();482crc.update(buffer, 4, 4); // only chunkType483}484485/**486* Open a chunk of an expected type487*488* @param expected The expected type of the next chunk489* @throws IOException Indicate a failure to read data or a different chunk on the stream490*/491private void openChunk(int expected) throws IOException {492openChunk();493if(chunkType != expected) {494throw new IOException("Expected chunk: " + Integer.toHexString(expected));495}496}497498/**499* Check the current chunk has the correct size500*501* @param expected The expected size of the chunk502* @throws IOException Indicate an invalid size503*/504private void checkChunkLength(int expected) throws IOException {505if(chunkLength != expected) {506throw new IOException("Chunk has wrong size");507}508}509510/**511* Read some data from the current chunk512*513* @param buffer The buffer to read into514* @param offset The offset into the buffer to read into515* @param length The amount of data to read516* @return The number of bytes read from the chunk517* @throws IOException Indicate a failure to read the appropriate data from the chunk518*/519private int readChunk(byte[] buffer, int offset, int length) throws IOException {520if(length > chunkRemaining) {521length = chunkRemaining;522}523readFully(buffer, offset, length);524crc.update(buffer, offset, length);525chunkRemaining -= length;526return length;527}528529/**530* Refill the inflating stream with data from the stream531*532* @param inflater The inflater to fill533* @throws IOException Indicates there is no more data left or invalid data has been found on534* the stream.535*/536private void refillInflater(Inflater inflater) throws IOException {537while(chunkRemaining == 0) {538closeChunk();539openChunk(IDAT);540}541int read = readChunk(buffer, 0, buffer.length);542inflater.setInput(buffer, 0, read);543}544545/**546* Read a chunk from the inflater547*548* @param inflater The inflater to read the data from549* @param buffer The buffer to write into550* @param offset The offset into the buffer at which to start writing551* @param length The number of bytes to read552* @throws IOException Indicates a failure to read the complete chunk553*/554private void readChunkUnzip(Inflater inflater, byte[] buffer, int offset, int length) throws IOException {555try {556do {557int read = inflater.inflate(buffer, offset, length);558if(read <= 0) {559if(inflater.finished()) {560throw new EOFException();561}562if(inflater.needsInput()) {563refillInflater(inflater);564} else {565throw new IOException("Can't inflate " + length + " bytes");566}567} else {568offset += read;569length -= read;570}571} while(length > 0);572} catch (DataFormatException ex) {573IOException io = new IOException("inflate error");574io.initCause(ex);575576throw io;577}578}579580/**581* Read a complete buffer of data from the input stream582*583* @param buffer The buffer to read into584* @param offset The offset to start copying into585* @param length The length of bytes to read586* @throws IOException Indicates a failure to access the data587*/588private void readFully(byte[] buffer, int offset, int length) throws IOException {589do {590int read = input.read(buffer, offset, length);591if(read < 0) {592throw new EOFException();593}594offset += read;595length -= read;596} while(length > 0);597}598599/**600* Read an int from a buffer601*602* @param buffer The buffer to read from603* @param offset The offset into the buffer to read from604* @return The int read interpreted in big endian605*/606private int readInt(byte[] buffer, int offset) {607return608((buffer[offset ] ) << 24) |609((buffer[offset+1] & 255) << 16) |610((buffer[offset+2] & 255) << 8) |611((buffer[offset+3] & 255) );612}613614/**615* Check the signature of the PNG to confirm it's a PNG616*617* @param buffer The buffer to read from618* @return True if the PNG signature is correct619*/620private boolean checkSignatur(byte[] buffer) {621for(int i=0 ; i<SIGNATURE.length ; i++) {622if(buffer[i] != SIGNATURE[i]) {623return false;624}625}626return true;627}628629/**630* @see org.newdawn.slick.opengl.ImageData#getDepth()631*/632public int getDepth() {633return bitDepth;634}635636/**637* @see org.newdawn.slick.opengl.ImageData#getImageBufferData()638*/639public ByteBuffer getImageBufferData() {640return scratch;641}642643/**644* @see org.newdawn.slick.opengl.ImageData#getTexHeight()645*/646public int getTexHeight() {647return texHeight;648}649650/**651* @see org.newdawn.slick.opengl.ImageData#getTexWidth()652*/653public int getTexWidth() {654return texWidth;655}656657/**658* @see org.newdawn.slick.opengl.LoadableImageData#loadImage(java.io.InputStream)659*/660public ByteBuffer loadImage(InputStream fis) throws IOException {661return loadImage(fis, false, null);662}663664/**665* @see org.newdawn.slick.opengl.LoadableImageData#loadImage(java.io.InputStream, boolean, int[])666*/667public ByteBuffer loadImage(InputStream fis, boolean flipped, int[] transparent) throws IOException {668return loadImage(fis, flipped, false, transparent);669}670671/**672* @see org.newdawn.slick.opengl.LoadableImageData#loadImage(java.io.InputStream, boolean, boolean, int[])673*/674public ByteBuffer loadImage(InputStream fis, boolean flipped, boolean forceAlpha, int[] transparent) throws IOException {675if (transparent != null) {676forceAlpha = true;677}678679init(fis);680681if (!isRGB()) {682throw new IOException("Only RGB formatted images are supported by the PNGLoader");683}684685texWidth = get2Fold(width);686texHeight = get2Fold(height);687688int perPixel = hasAlpha() ? 4 : 3;689690// Get a pointer to the image memory691scratch = BufferUtils.createByteBuffer(texWidth * texHeight * perPixel);692decode(scratch, texWidth * perPixel, flipped);693694if (height < texHeight-1) {695int topOffset = (texHeight-1) * (texWidth*perPixel);696int bottomOffset = (height-1) * (texWidth*perPixel);697for (int x=0;x<texWidth;x++) {698for (int i=0;i<perPixel;i++) {699scratch.put(topOffset+x+i, scratch.get(x+i));700scratch.put(bottomOffset+(texWidth*perPixel)+x+i, scratch.get(bottomOffset+x+i));701}702}703}704if (width < texWidth-1) {705for (int y=0;y<texHeight;y++) {706for (int i=0;i<perPixel;i++) {707scratch.put(((y+1)*(texWidth*perPixel))-perPixel+i, scratch.get(y*(texWidth*perPixel)+i));708scratch.put((y*(texWidth*perPixel))+(width*perPixel)+i, scratch.get((y*(texWidth*perPixel))+((width-1)*perPixel)+i));709}710}711}712713if (!hasAlpha() && forceAlpha) {714ByteBuffer temp = BufferUtils.createByteBuffer(texWidth * texHeight * 4);715for (int x=0;x<texWidth;x++) {716for (int y=0;y<texHeight;y++) {717int srcOffset = (y*3)+(x*texHeight*3);718int dstOffset = (y*4)+(x*texHeight*4);719720temp.put(dstOffset, scratch.get(srcOffset));721temp.put(dstOffset+1, scratch.get(srcOffset+1));722temp.put(dstOffset+2, scratch.get(srcOffset+2));723temp.put(dstOffset+3, (byte) 255);724}725}726727colorType = COLOR_TRUEALPHA;728bitDepth = 32;729scratch = temp;730}731732if (transparent != null) {733for (int i=0;i<texWidth*texHeight*4;i+=4) {734boolean match = true;735for (int c=0;c<3;c++) {736if (toInt(scratch.get(i+c)) != transparent[c]) {737match = false;738}739}740741if (match) {742scratch.put(i+3, (byte) 0);743}744}745}746747scratch.position(0);748749return scratch;750}751752/**753* Safe convert byte to int754*755* @param b The byte to convert756* @return The converted byte757*/758private int toInt(byte b) {759if (b < 0) {760return 256+b;761}762763return b;764}765766/**767* Get the closest greater power of 2 to the fold number768*769* @param fold The target number770* @return The power of 2771*/772private int get2Fold(int fold) {773int ret = 2;774while (ret < fold) {775ret *= 2;776}777return ret;778}779780/**781* @see org.newdawn.slick.opengl.LoadableImageData#configureEdging(boolean)782*/783public void configureEdging(boolean edging) {784}785}786787788789