package loon; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.zip.CRC32; import java.util.zip.DataFormatException; import java.util.zip.Inflater; import loon.core.graphics.opengl.GLEx; import org.lwjgl.BufferUtils; public class JavaSEPNGData { static class PNGDecoder { public static Format ALPHA = new Format(1, true); public static Format LUMINANCE = new Format(1, false); public static Format LUMINANCE_ALPHA = new Format(2, true); public static Format RGB = new Format(3, false); public static Format RGBA = new Format(4, true); public static Format BGRA = new Format(4, true); public static Format ABGR = new Format(4, true); public static class Format { final int numComponents; final boolean hasAlpha; private Format(int numComponents, boolean hasAlpha) { this.numComponents = numComponents; this.hasAlpha = hasAlpha; } public int getNumComponents() { return numComponents; } public boolean isHasAlpha() { return hasAlpha; } } private static final byte[] SIGNATURE = { (byte) 137, 80, 78, 71, 13, 10, 26, 10 }; private static final int IHDR = 0x49484452; private static final int PLTE = 0x504C5445; private static final int tRNS = 0x74524E53; private static final int IDAT = 0x49444154; private static final byte COLOR_GREYSCALE = 0; private static final byte COLOR_TRUECOLOR = 2; private static final byte COLOR_INDEXED = 3; private static final byte COLOR_GREYALPHA = 4; private static final byte COLOR_TRUEALPHA = 6; private final InputStream input; private final CRC32 crc; private final byte[] buffer; private int chunkLength; private int chunkType; private int chunkRemaining; private int width; private int height; private int bitdepth; private int colorType; private int bytesPerPixel; private byte[] palette; private byte[] paletteA; private byte[] transPixel; public PNGDecoder(InputStream input) throws IOException { this.input = input; this.crc = new CRC32(); this.buffer = new byte[4096]; readFully(buffer, 0, SIGNATURE.length); if (!checkSignature(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(); } if (colorType == COLOR_INDEXED && palette == null) { throw new IOException("Missing PLTE chunk"); } } public int getHeight() { return height; } public int getWidth() { return width; } public boolean hasAlpha() { return colorType == COLOR_TRUEALPHA || paletteA != null || transPixel != null; } public boolean isRGB() { return colorType == COLOR_TRUEALPHA || colorType == COLOR_TRUECOLOR || colorType == COLOR_INDEXED; } public Format decideTextureFormat(Format fmt) { switch (colorType) { case COLOR_TRUECOLOR: if ((fmt == ABGR) || (fmt == RGBA) || (fmt == BGRA) || (fmt == RGB)) { return fmt; } return RGB; case COLOR_TRUEALPHA: if ((fmt == ABGR) || (fmt == RGBA) || (fmt == BGRA) || (fmt == RGB)) { return fmt; } return RGBA; case COLOR_GREYSCALE: if ((fmt == LUMINANCE) || (fmt == ALPHA)) { return fmt; } return LUMINANCE; case COLOR_GREYALPHA: return LUMINANCE_ALPHA; case COLOR_INDEXED: if ((fmt == ABGR) || (fmt == RGBA) || (fmt == BGRA)) { return fmt; } return RGBA; default: throw new UnsupportedOperationException("Not yet implemented"); } } public void decode(ByteBuffer buffer, int stride, Format fmt) throws IOException { final int offset = buffer.position(); final int lineSize = ((width * bitdepth + 7) / 8) * bytesPerPixel; byte[] curLine = new byte[lineSize + 1]; byte[] prevLine = new byte[lineSize + 1]; byte[] palLine = (bitdepth < 8) ? new byte[width + 1] : null; final Inflater inflater = new Inflater(); try { for (int y = 0; y < height; y++) { readChunkUnzip(inflater, curLine, 0, curLine.length); unfilter(curLine, prevLine); buffer.position(offset + y * stride); switch (colorType) { case COLOR_TRUECOLOR: if (fmt == ABGR) { copyRGBtoABGR(buffer, curLine); } else if (fmt == RGBA) { copyRGBtoRGBA(buffer, curLine); } else if (fmt == BGRA) { copyRGBtoBGRA(buffer, curLine); } else if (fmt == RGB) { copy(buffer, curLine); } else { throw new UnsupportedOperationException( "Unsupported format for this image"); } break; case COLOR_TRUEALPHA: if (fmt == ABGR) { copyRGBAtoABGR(buffer, curLine); } else if (fmt == RGBA) { copy(buffer, curLine); } else if (fmt == BGRA) { copyRGBAtoBGRA(buffer, curLine); break; } else if (fmt == RGB) { copyRGBAtoRGB(buffer, curLine); break; } else { throw new UnsupportedOperationException( "Unsupported format for this image"); } break; case COLOR_GREYSCALE: if ((fmt == LUMINANCE) || (fmt == ALPHA)) { copy(buffer, curLine); } else { throw new UnsupportedOperationException( "Unsupported format for this image"); } break; case COLOR_GREYALPHA: if (fmt == LUMINANCE_ALPHA) { copy(buffer, curLine); } else { throw new UnsupportedOperationException( "Unsupported format for this image"); } break; case COLOR_INDEXED: switch (bitdepth) { case 8: palLine = curLine; break; case 4: expand4(curLine, palLine); break; case 2: expand2(curLine, palLine); break; case 1: expand1(curLine, palLine); break; default: throw new UnsupportedOperationException( "Unsupported bitdepth for this image"); } if (fmt == ABGR) { copyPALtoABGR(buffer, palLine); } else if (fmt == RGBA) { copyPALtoRGBA(buffer, palLine); } else if (fmt == BGRA) { copyPALtoBGRA(buffer, palLine); } else { throw new UnsupportedOperationException( "Unsupported format for this image"); } break; default: throw new UnsupportedOperationException( "Not yet implemented"); } byte[] tmp = curLine; curLine = prevLine; prevLine = tmp; } } finally { inflater.end(); } } private void copy(ByteBuffer buffer, byte[] curLine) { buffer.put(curLine, 1, curLine.length - 1); } private void copyRGBtoABGR(ByteBuffer buffer, byte[] curLine) { if (transPixel != null) { byte tr = transPixel[1]; byte tg = transPixel[3]; byte tb = transPixel[5]; for (int i = 1, n = curLine.length; i < n; i += 3) { byte r = curLine[i]; byte g = curLine[i + 1]; byte b = curLine[i + 2]; byte a = (byte) 0xFF; if (r == tr && g == tg && b == tb) { a = 0; } buffer.put(a).put(b).put(g).put(r); } } else { for (int i = 1, n = curLine.length; i < n; i += 3) { buffer.put((byte) 0xFF).put(curLine[i + 2]) .put(curLine[i + 1]).put(curLine[i]); } } } private void copyRGBtoRGBA(ByteBuffer buffer, byte[] curLine) { if (transPixel != null) { byte tr = transPixel[1]; byte tg = transPixel[3]; byte tb = transPixel[5]; for (int i = 1, n = curLine.length; i < n; i += 3) { byte r = curLine[i]; byte g = curLine[i + 1]; byte b = curLine[i + 2]; byte a = (byte) 0xFF; if (r == tr && g == tg && b == tb) { a = 0; } buffer.put(r).put(g).put(b).put(a); } } else { for (int i = 1, n = curLine.length; i < n; i += 3) { buffer.put(curLine[i]).put(curLine[i + 1]) .put(curLine[i + 2]).put((byte) 0xFF); } } } private void copyRGBtoBGRA(ByteBuffer buffer, byte[] curLine) { if (transPixel != null) { byte tr = transPixel[1]; byte tg = transPixel[3]; byte tb = transPixel[5]; for (int i = 1, n = curLine.length; i < n; i += 3) { byte r = curLine[i]; byte g = curLine[i + 1]; byte b = curLine[i + 2]; byte a = (byte) 0xFF; if (r == tr && g == tg && b == tb) { a = 0; } buffer.put(b).put(g).put(r).put(a); } } else { for (int i = 1, n = curLine.length; i < n; i += 3) { buffer.put(curLine[i + 2]).put(curLine[i + 1]) .put(curLine[i]).put((byte) 0xFF); } } } private void copyRGBAtoABGR(ByteBuffer buffer, byte[] curLine) { for (int i = 1, n = curLine.length; i < n; i += 4) { buffer.put(curLine[i + 3]).put(curLine[i + 2]) .put(curLine[i + 1]).put(curLine[i]); } } private void copyRGBAtoBGRA(ByteBuffer buffer, byte[] curLine) { for (int i = 1, n = curLine.length; i < n; i += 4) { buffer.put(curLine[i + 2]).put(curLine[i + 1]) .put(curLine[i + 0]).put(curLine[i + 3]); } } private void copyRGBAtoRGB(ByteBuffer buffer, byte[] curLine) { for (int i = 1, n = curLine.length; i < n; i += 4) { buffer.put(curLine[i]).put(curLine[i + 1]).put(curLine[i + 2]); } } private void copyPALtoABGR(ByteBuffer buffer, byte[] curLine) { if (paletteA != null) { for (int i = 1, n = curLine.length; i < n; i += 1) { int idx = curLine[i] & 255; byte r = palette[idx * 3 + 0]; byte g = palette[idx * 3 + 1]; byte b = palette[idx * 3 + 2]; byte a = paletteA[idx]; buffer.put(a).put(b).put(g).put(r); } } else { for (int i = 1, n = curLine.length; i < n; i += 1) { int idx = curLine[i] & 255; byte r = palette[idx * 3 + 0]; byte g = palette[idx * 3 + 1]; byte b = palette[idx * 3 + 2]; byte a = (byte) 0xFF; buffer.put(a).put(b).put(g).put(r); } } } private void copyPALtoRGBA(ByteBuffer buffer, byte[] curLine) { if (paletteA != null) { for (int i = 1, n = curLine.length; i < n; i += 1) { int idx = curLine[i] & 255; byte r = palette[idx * 3 + 0]; byte g = palette[idx * 3 + 1]; byte b = palette[idx * 3 + 2]; byte a = paletteA[idx]; buffer.put(r).put(g).put(b).put(a); } } else { for (int i = 1, n = curLine.length; i < n; i += 1) { int idx = curLine[i] & 255; byte r = palette[idx * 3 + 0]; byte g = palette[idx * 3 + 1]; byte b = palette[idx * 3 + 2]; byte a = (byte) 0xFF; buffer.put(r).put(g).put(b).put(a); } } } private void copyPALtoBGRA(ByteBuffer buffer, byte[] curLine) { if (paletteA != null) { for (int i = 1, n = curLine.length; i < n; i += 1) { int idx = curLine[i] & 255; byte r = palette[idx * 3 + 0]; byte g = palette[idx * 3 + 1]; byte b = palette[idx * 3 + 2]; byte a = paletteA[idx]; buffer.put(b).put(g).put(r).put(a); } } else { for (int i = 1, n = curLine.length; i < n; i += 1) { int idx = curLine[i] & 255; byte r = palette[idx * 3 + 0]; byte g = palette[idx * 3 + 1]; byte b = palette[idx * 3 + 2]; byte a = (byte) 0xFF; buffer.put(b).put(g).put(r).put(a); } } } private void expand4(byte[] src, byte[] dst) { for (int i = 1, n = dst.length; i < n; i += 2) { int val = src[1 + (i >> 1)] & 255; switch (n - i) { default: dst[i + 1] = (byte) (val & 15); case 1: dst[i] = (byte) (val >> 4); } } } private void expand2(byte[] src, byte[] dst) { for (int i = 1, n = dst.length; i < n; i += 4) { int val = src[1 + (i >> 2)] & 255; switch (n - i) { default: dst[i + 3] = (byte) ((val) & 3); case 3: dst[i + 2] = (byte) ((val >> 2) & 3); case 2: dst[i + 1] = (byte) ((val >> 4) & 3); case 1: dst[i] = (byte) ((val >> 6)); } } } private void expand1(byte[] src, byte[] dst) { for (int i = 1, n = dst.length; i < n; i += 8) { int val = src[1 + (i >> 3)] & 255; switch (n - i) { default: dst[i + 7] = (byte) ((val) & 1); case 7: dst[i + 6] = (byte) ((val >> 1) & 1); case 6: dst[i + 5] = (byte) ((val >> 2) & 1); case 5: dst[i + 4] = (byte) ((val >> 3) & 1); case 4: dst[i + 3] = (byte) ((val >> 4) & 1); case 3: dst[i + 2] = (byte) ((val >> 5) & 1); case 2: dst[i + 1] = (byte) ((val >> 6) & 1); case 1: dst[i] = (byte) ((val >> 7)); } } } 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]); } } private void unfilterSub(byte[] curLine) { final int bpp = this.bytesPerPixel; for (int i = bpp + 1, n = curLine.length; i < n; ++i) { curLine[i] += curLine[i - bpp]; } } private void unfilterUp(byte[] curLine, byte[] prevLine) { for (int i = 1, n = curLine.length; i < n; ++i) { curLine[i] += prevLine[i]; } } private void unfilterAverage(byte[] curLine, byte[] prevLine) { final int bpp = this.bytesPerPixel; int i; for (i = 1; i <= bpp; ++i) { curLine[i] += (byte) ((prevLine[i] & 0xFF) >>> 1); } for (int n = curLine.length; i < n; ++i) { curLine[i] += (byte) (((prevLine[i] & 0xFF) + (curLine[i - bpp] & 0xFF)) >>> 1); } } private void unfilterPaeth(byte[] curLine, byte[] prevLine) { final int bpp = this.bytesPerPixel; int i; for (i = 1; i <= bpp; ++i) { curLine[i] += prevLine[i]; } for (int n = curLine.length; i < n; ++i) { int a = curLine[i - bpp] & 255; int b = prevLine[i] & 255; int c = prevLine[i - bpp] & 255; 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; } } private void readIHDR() throws IOException { checkChunkLength(13); readChunk(buffer, 0, 13); width = readInt(buffer, 0); height = readInt(buffer, 4); bitdepth = buffer[8] & 255; colorType = buffer[9] & 255; switch (colorType) { case COLOR_GREYSCALE: if (bitdepth != 8) { throw new IOException("Unsupported bit depth: " + bitdepth); } bytesPerPixel = 1; break; case COLOR_GREYALPHA: if (bitdepth != 8) { throw new IOException("Unsupported bit depth: " + bitdepth); } bytesPerPixel = 2; break; case COLOR_TRUECOLOR: if (bitdepth != 8) { throw new IOException("Unsupported bit depth: " + bitdepth); } bytesPerPixel = 3; break; case COLOR_TRUEALPHA: if (bitdepth != 8) { throw new IOException("Unsupported bit depth: " + bitdepth); } bytesPerPixel = 4; break; case COLOR_INDEXED: switch (bitdepth) { case 8: case 4: case 2: case 1: bytesPerPixel = 1; break; default: throw new IOException("Unsupported bit depth: " + bitdepth); } break; default: throw new IOException("unsupported color format: " + colorType); } 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"); } } 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); } 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]; Arrays.fill(paletteA, (byte) 0xFF); readChunk(paletteA, 0, paletteA.length); break; default: } } private void closeChunk() throws IOException { if (chunkRemaining > 0) { 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; } 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); } private void openChunk(int expected) throws IOException { openChunk(); if (chunkType != expected) { throw new IOException("Expected chunk: " + Integer.toHexString(expected)); } } private void checkChunkLength(int expected) throws IOException { if (chunkLength != expected) { throw new IOException("Chunk has wrong size"); } } 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; } 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); } 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) { throw (IOException) (new IOException("inflate error") .initCause(ex)); } } 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); } 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)); } private void skip(long amount) throws IOException { while (amount > 0) { long skipped = input.skip(amount); if (skipped < 0) { throw new EOFException(); } amount -= skipped; } } private static boolean checkSignature(byte[] buffer) { for (int i = 0; i < SIGNATURE.length; i++) { if (buffer[i] != SIGNATURE[i]) { return false; } } return true; } } private int width; private int height; private int texHeight; private int texWidth; private int bitDepth; private boolean hasAlpha; private ByteBuffer scratch; public int getDepth() { return bitDepth; } public ByteBuffer getImageBufferData() { return scratch; } public int getTexHeight() { return texHeight; } public int getTexWidth() { return texWidth; } public ByteBuffer loadImage(InputStream fis) throws IOException { return loadImage(fis, false, null); } public ByteBuffer loadImage(InputStream fis, boolean flipped, int[] transparent) throws IOException { return loadImage(fis, flipped, false, transparent); } public ByteBuffer loadImage(InputStream fis, boolean flipped, boolean forceAlpha, int[] transparent) throws IOException { PNGDecoder decoder = new PNGDecoder(fis); if (!decoder.isRGB()) { throw new IOException( "Only RGB formatted images are supported by the PNGLoader"); } width = decoder.getWidth(); height = decoder.getHeight(); texWidth = GLEx.toPowerOfTwo(width); texHeight = GLEx.toPowerOfTwo(height); hasAlpha = decoder.hasAlpha(); int perPixel = decoder.hasAlpha() ? 4 : 3; bitDepth = decoder.hasAlpha() ? 32 : 24; scratch = BufferUtils.createByteBuffer(texWidth * texHeight * perPixel); decoder.decode(scratch, texWidth * perPixel, perPixel == 4 ? PNGDecoder.RGBA : PNGDecoder.RGB); 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 (!decoder.hasAlpha() && forceAlpha) { ByteBuffer temp = BufferUtils.createByteBuffer(texWidth * texHeight * 4); 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)); if ((x < getHeight()) && (y < getWidth())) { temp.put(dstOffset + 3, (byte) 255); } else { temp.put(dstOffset + 3, (byte) 0); } } } 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; } private int toInt(byte b) { if (b < 0) { return 256 + b; } return b; } public void configureEdging(boolean edging) { } public int getWidth() { return width; } public int getHeight() { return height; } public boolean hasAlpha() { return hasAlpha; } }