/******************************************************************************* * Copyright (C) 2013 JMaNGOS <http://jmangos.org/> * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package org.jmangos.tools.openGL; 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; public class PNGDecoder { public enum Format { ALPHA(1, true), LUMINANCE(1, false), LUMINANCE_ALPHA(2, true), RGB(3, false), RGBA(4, true), BGRA(4, true), ABGR(4, true); final int numComponents; final boolean hasAlpha; private Format(final int numComponents, final boolean hasAlpha) { this.numComponents = numComponents; this.hasAlpha = hasAlpha; } public int getNumComponents() { return this.numComponents; } public boolean isHasAlpha() { return this.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; @SuppressWarnings("unused") private static final int IEND = 0x49454E44; 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(final InputStream input) throws IOException { this.input = input; this.crc = new CRC32(); this.buffer = new byte[4096]; readFully(this.buffer, 0, SIGNATURE.length); if (!checkSignature(this.buffer)) { throw new IOException("Not a valid PNG file"); } openChunk(IHDR); readIHDR(); closeChunk(); searchIDAT: for (;;) { openChunk(); switch (this.chunkType) { case IDAT: break searchIDAT; case PLTE: readPLTE(); break; case tRNS: readtRNS(); break; } closeChunk(); } if ((this.colorType == COLOR_INDEXED) && (this.palette == null)) { throw new IOException("Missing PLTE chunk"); } } public int getHeight() { return this.height; } public int getWidth() { return this.width; } /** * Checks if the image has a real alpha channel. This method does not check * for the presence of * a tRNS chunk. * * @return true if the image has an alpha channel * @see #hasAlpha() */ public boolean hasAlphaChannel() { return (this.colorType == COLOR_TRUEALPHA) || (this.colorType == COLOR_GREYALPHA); } /** * Checks if the image has transparency information either from an alpha * channel or from a tRNS * chunk. * * @return true if the image has transparency * @see #hasAlphaChannel() * @see #overwriteTRNS(byte, byte, byte) */ public boolean hasAlpha() { return hasAlphaChannel() || (this.paletteA != null) || (this.transPixel != null); } public boolean isRGB() { return (this.colorType == COLOR_TRUEALPHA) || (this.colorType == COLOR_TRUECOLOR) || (this.colorType == COLOR_INDEXED); } /** * Overwrites the tRNS chunk entry to make a selected color transparent. * <p> * This can only be invoked when the image has no alpha channel. * </p> * <p> * Calling this method causes {@link #hasAlpha()} to return true. * </p> * * @param r * the red component of the color to make transparent * @param g * the green component of the color to make transparent * @param b * the blue component of the color to make transparent * @throws UnsupportedOperationException * if the tRNS chunk data can't be set * @see #hasAlphaChannel() */ public void overwriteTRNS(final byte r, final byte g, final byte b) { if (hasAlphaChannel()) { throw new UnsupportedOperationException("image has an alpha channel"); } final byte[] pal = this.palette; if (pal == null) { this.transPixel = new byte[] { 0, r, 0, g, 0, b }; } else { this.paletteA = new byte[pal.length / 3]; for (int i = 0, j = 0; i < pal.length; i += 3, j++) { if ((pal[i] != r) || (pal[i + 1] != g) || (pal[i + 2] != b)) { this.paletteA[j] = (byte) 0xFF; } } } } /** * Computes the implemented format conversion for the desired format. * * @param fmt * the desired format * @return format which best matches the desired format * @throws UnsupportedOperationException * if this PNG file can't be decoded */ public Format decideTextureFormat(final Format fmt) { switch (this.colorType) { case COLOR_TRUECOLOR: switch (fmt) { case ABGR: case RGBA: case BGRA: case RGB: return fmt; default: return Format.RGB; } case COLOR_TRUEALPHA: switch (fmt) { case ABGR: case RGBA: case BGRA: case RGB: return fmt; default: return Format.RGBA; } case COLOR_GREYSCALE: switch (fmt) { case LUMINANCE: case ALPHA: return fmt; default: return Format.LUMINANCE; } case COLOR_GREYALPHA: return Format.LUMINANCE_ALPHA; case COLOR_INDEXED: switch (fmt) { case ABGR: case RGBA: case BGRA: return fmt; default: return Format.RGBA; } default: throw new UnsupportedOperationException("Not yet implemented"); } } /** * Decodes the image into the specified buffer. The first line is placed at * the current * position. After decode the buffer position is at the end of the last * line. * * @param buffer * the buffer * @param stride * the stride in bytes from start of a line to start of the next * line, can be * negative. * @param fmt * the target format into which the image should be decoded. * @throws IOException * if a read or data error occurred * @throws IllegalArgumentException * if the start position of a line falls outside the buffer * @throws UnsupportedOperationException * if the image can't be decoded into the desired format */ public void decode(final ByteBuffer buffer, final int stride, final Format fmt) throws IOException { final int offset = buffer.position(); final int lineSize = (((this.width * this.bitdepth) + 7) / 8) * this.bytesPerPixel; byte[] curLine = new byte[lineSize + 1]; byte[] prevLine = new byte[lineSize + 1]; byte[] palLine = (this.bitdepth < 8) ? new byte[this.width + 1] : null; final Inflater inflater = new Inflater(); try { for (int y = 0; y < this.height; y++) { readChunkUnzip(inflater, curLine, 0, curLine.length); unfilter(curLine, prevLine); buffer.position(offset + (y * stride)); switch (this.colorType) { case COLOR_TRUECOLOR: switch (fmt) { case ABGR: copyRGBtoABGR(buffer, curLine); break; case RGBA: copyRGBtoRGBA(buffer, curLine); break; case BGRA: copyRGBtoBGRA(buffer, curLine); break; case RGB: copy(buffer, curLine); break; default: throw new UnsupportedOperationException( "Unsupported format for this image"); } break; case COLOR_TRUEALPHA: switch (fmt) { case ABGR: copyRGBAtoABGR(buffer, curLine); break; case RGBA: copy(buffer, curLine); break; case BGRA: copyRGBAtoBGRA(buffer, curLine); break; case RGB: copyRGBAtoRGB(buffer, curLine); break; default: throw new UnsupportedOperationException( "Unsupported format for this image"); } break; case COLOR_GREYSCALE: switch (fmt) { case LUMINANCE: case ALPHA: copy(buffer, curLine); break; default: throw new UnsupportedOperationException( "Unsupported format for this image"); } break; case COLOR_GREYALPHA: switch (fmt) { case LUMINANCE_ALPHA: copy(buffer, curLine); break; default: throw new UnsupportedOperationException( "Unsupported format for this image"); } break; case COLOR_INDEXED: switch (this.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"); } switch (fmt) { case ABGR: copyPALtoABGR(buffer, palLine); break; case RGBA: copyPALtoRGBA(buffer, palLine); break; case BGRA: copyPALtoBGRA(buffer, palLine); break; default: throw new UnsupportedOperationException( "Unsupported format for this image"); } break; default: throw new UnsupportedOperationException("Not yet implemented"); } final byte[] tmp = curLine; curLine = prevLine; prevLine = tmp; } } finally { inflater.end(); } } /** * Decodes the image into the specified buffer. The last line is placed at * the current position. * After decode the buffer position is at the end of the first line. * * @param buffer * the buffer * @param stride * the stride in bytes from start of a line to start of the next * line, must be * positive. * @param fmt * the target format into which the image should be decoded. * @throws IOException * if a read or data error occurred * @throws IllegalArgumentException * if the start position of a line falls outside the buffer * @throws UnsupportedOperationException * if the image can't be decoded into the desired format */ public void decodeFlipped(final ByteBuffer buffer, final int stride, final Format fmt) throws IOException { if (stride <= 0) { throw new IllegalArgumentException("stride"); } final int pos = buffer.position(); final int posDelta = (this.height - 1) * stride; buffer.position(pos + posDelta); decode(buffer, -stride, fmt); buffer.position(buffer.position() + posDelta); } private void copy(final ByteBuffer buffer, final byte[] curLine) { buffer.put(curLine, 1, curLine.length - 1); } private void copyRGBtoABGR(final ByteBuffer buffer, final byte[] curLine) { if (this.transPixel != null) { final byte tr = this.transPixel[1]; final byte tg = this.transPixel[3]; final byte tb = this.transPixel[5]; for (int i = 1, n = curLine.length; i < n; i += 3) { final byte r = curLine[i]; final byte g = curLine[i + 1]; final 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(final ByteBuffer buffer, final byte[] curLine) { if (this.transPixel != null) { final byte tr = this.transPixel[1]; final byte tg = this.transPixel[3]; final byte tb = this.transPixel[5]; for (int i = 1, n = curLine.length; i < n; i += 3) { final byte r = curLine[i]; final byte g = curLine[i + 1]; final 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(final ByteBuffer buffer, final byte[] curLine) { if (this.transPixel != null) { final byte tr = this.transPixel[1]; final byte tg = this.transPixel[3]; final byte tb = this.transPixel[5]; for (int i = 1, n = curLine.length; i < n; i += 3) { final byte r = curLine[i]; final byte g = curLine[i + 1]; final 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(final ByteBuffer buffer, final 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(final ByteBuffer buffer, final 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]).put(curLine[i + 3]); } } private void copyRGBAtoRGB(final ByteBuffer buffer, final 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(final ByteBuffer buffer, final byte[] curLine) { if (this.paletteA != null) { for (int i = 1, n = curLine.length; i < n; i += 1) { final int idx = curLine[i] & 255; final byte r = this.palette[(idx * 3) + 0]; final byte g = this.palette[(idx * 3) + 1]; final byte b = this.palette[(idx * 3) + 2]; final byte a = this.paletteA[idx]; buffer.put(a).put(b).put(g).put(r); } } else { for (int i = 1, n = curLine.length; i < n; i += 1) { final int idx = curLine[i] & 255; final byte r = this.palette[(idx * 3) + 0]; final byte g = this.palette[(idx * 3) + 1]; final byte b = this.palette[(idx * 3) + 2]; final byte a = (byte) 0xFF; buffer.put(a).put(b).put(g).put(r); } } } private void copyPALtoRGBA(final ByteBuffer buffer, final byte[] curLine) { if (this.paletteA != null) { for (int i = 1, n = curLine.length; i < n; i += 1) { final int idx = curLine[i] & 255; final byte r = this.palette[(idx * 3) + 0]; final byte g = this.palette[(idx * 3) + 1]; final byte b = this.palette[(idx * 3) + 2]; final byte a = this.paletteA[idx]; buffer.put(r).put(g).put(b).put(a); } } else { for (int i = 1, n = curLine.length; i < n; i += 1) { final int idx = curLine[i] & 255; final byte r = this.palette[(idx * 3) + 0]; final byte g = this.palette[(idx * 3) + 1]; final byte b = this.palette[(idx * 3) + 2]; final byte a = (byte) 0xFF; buffer.put(r).put(g).put(b).put(a); } } } private void copyPALtoBGRA(final ByteBuffer buffer, final byte[] curLine) { if (this.paletteA != null) { for (int i = 1, n = curLine.length; i < n; i += 1) { final int idx = curLine[i] & 255; final byte r = this.palette[(idx * 3) + 0]; final byte g = this.palette[(idx * 3) + 1]; final byte b = this.palette[(idx * 3) + 2]; final byte a = this.paletteA[idx]; buffer.put(b).put(g).put(r).put(a); } } else { for (int i = 1, n = curLine.length; i < n; i += 1) { final int idx = curLine[i] & 255; final byte r = this.palette[(idx * 3) + 0]; final byte g = this.palette[(idx * 3) + 1]; final byte b = this.palette[(idx * 3) + 2]; final byte a = (byte) 0xFF; buffer.put(b).put(g).put(r).put(a); } } } private void expand4(final byte[] src, final byte[] dst) { for (int i = 1, n = dst.length; i < n; i += 2) { final 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(final byte[] src, final byte[] dst) { for (int i = 1, n = dst.length; i < n; i += 4) { final 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(final byte[] src, final byte[] dst) { for (int i = 1, n = dst.length; i < n; i += 8) { final 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(final byte[] curLine, final 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(final 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(final byte[] curLine, final byte[] prevLine) { for (int i = 1, n = curLine.length; i < n; ++i) { curLine[i] += prevLine[i]; } } private void unfilterAverage(final byte[] curLine, final byte[] prevLine) { final int bpp = this.bytesPerPixel; int i; for (i = 1; i <= bpp; ++i) { curLine[i] += (byte) ((prevLine[i] & 0xFF) >>> 1); } for (final int n = curLine.length; i < n; ++i) { curLine[i] += (byte) (((prevLine[i] & 0xFF) + (curLine[i - bpp] & 0xFF)) >>> 1); } } private void unfilterPaeth(final byte[] curLine, final byte[] prevLine) { final int bpp = this.bytesPerPixel; int i; for (i = 1; i <= bpp; ++i) { curLine[i] += prevLine[i]; } for (final int n = curLine.length; i < n; ++i) { final int a = curLine[i - bpp] & 255; final int b = prevLine[i] & 255; int c = prevLine[i - bpp] & 255; final 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(this.buffer, 0, 13); this.width = readInt(this.buffer, 0); this.height = readInt(this.buffer, 4); this.bitdepth = this.buffer[8] & 255; this.colorType = this.buffer[9] & 255; switch (this.colorType) { case COLOR_GREYSCALE: if (this.bitdepth != 8) { throw new IOException("Unsupported bit depth: " + this.bitdepth); } this.bytesPerPixel = 1; break; case COLOR_GREYALPHA: if (this.bitdepth != 8) { throw new IOException("Unsupported bit depth: " + this.bitdepth); } this.bytesPerPixel = 2; break; case COLOR_TRUECOLOR: if (this.bitdepth != 8) { throw new IOException("Unsupported bit depth: " + this.bitdepth); } this.bytesPerPixel = 3; break; case COLOR_TRUEALPHA: if (this.bitdepth != 8) { throw new IOException("Unsupported bit depth: " + this.bitdepth); } this.bytesPerPixel = 4; break; case COLOR_INDEXED: switch (this.bitdepth) { case 8: case 4: case 2: case 1: this.bytesPerPixel = 1; break; default: throw new IOException("Unsupported bit depth: " + this.bitdepth); } break; default: throw new IOException("unsupported color format: " + this.colorType); } if (this.buffer[10] != 0) { throw new IOException("unsupported compression method"); } if (this.buffer[11] != 0) { throw new IOException("unsupported filtering method"); } if (this.buffer[12] != 0) { throw new IOException("unsupported interlace method"); } } private void readPLTE() throws IOException { final int paletteEntries = this.chunkLength / 3; if ((paletteEntries < 1) || (paletteEntries > 256) || ((this.chunkLength % 3) != 0)) { throw new IOException("PLTE chunk has wrong length"); } this.palette = new byte[paletteEntries * 3]; readChunk(this.palette, 0, this.palette.length); } private void readtRNS() throws IOException { switch (this.colorType) { case COLOR_GREYSCALE: checkChunkLength(2); this.transPixel = new byte[2]; readChunk(this.transPixel, 0, 2); break; case COLOR_TRUECOLOR: checkChunkLength(6); this.transPixel = new byte[6]; readChunk(this.transPixel, 0, 6); break; case COLOR_INDEXED: if (this.palette == null) { throw new IOException("tRNS chunk without PLTE chunk"); } this.paletteA = new byte[this.palette.length / 3]; Arrays.fill(this.paletteA, (byte) 0xFF); readChunk(this.paletteA, 0, this.paletteA.length); break; default: // just ignore it } } private void closeChunk() throws IOException { if (this.chunkRemaining > 0) { // just skip the rest and the CRC skip(this.chunkRemaining + 4); } else { readFully(this.buffer, 0, 4); final int expectedCrc = readInt(this.buffer, 0); final int computedCrc = (int) this.crc.getValue(); if (computedCrc != expectedCrc) { throw new IOException("Invalid CRC"); } } this.chunkRemaining = 0; this.chunkLength = 0; this.chunkType = 0; } private void openChunk() throws IOException { readFully(this.buffer, 0, 8); this.chunkLength = readInt(this.buffer, 0); this.chunkType = readInt(this.buffer, 4); this.chunkRemaining = this.chunkLength; this.crc.reset(); this.crc.update(this.buffer, 4, 4); // only chunkType } private void openChunk(final int expected) throws IOException { openChunk(); if (this.chunkType != expected) { throw new IOException("Expected chunk: " + Integer.toHexString(expected)); } } private void checkChunkLength(final int expected) throws IOException { if (this.chunkLength != expected) { throw new IOException("Chunk has wrong size"); } } private int readChunk(final byte[] buffer, final int offset, int length) throws IOException { if (length > this.chunkRemaining) { length = this.chunkRemaining; } readFully(buffer, offset, length); this.crc.update(buffer, offset, length); this.chunkRemaining -= length; return length; } private void refillInflater(final Inflater inflater) throws IOException { while (this.chunkRemaining == 0) { closeChunk(); openChunk(IDAT); } final int read = readChunk(this.buffer, 0, this.buffer.length); inflater.setInput(this.buffer, 0, read); } private void readChunkUnzip(final Inflater inflater, final byte[] buffer, int offset, int length) throws IOException { assert (buffer != this.buffer); try { do { final 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 (final DataFormatException ex) { throw (IOException) (new IOException("inflate error").initCause(ex)); } } private void readFully(final byte[] buffer, int offset, int length) throws IOException { do { final int read = this.input.read(buffer, offset, length); if (read < 0) { throw new EOFException(); } offset += read; length -= read; } while (length > 0); } private int readInt(final byte[] buffer, final 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) { final long skipped = this.input.skip(amount); if (skipped < 0) { throw new EOFException(); } amount -= skipped; } } private static boolean checkSignature(final byte[] buffer) { for (int i = 0; i < SIGNATURE.length; i++) { if (buffer[i] != SIGNATURE[i]) { return false; } } return true; } }