diff --git a/.checkstyle b/.checkstyle new file mode 100644 index 0000000..428926e --- /dev/null +++ b/.checkstyle @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..cd01105 --- /dev/null +++ b/.classpath @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d075be7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +/bin/ +/Operator/ +/DrawerProperties/ +*.pdfd +*.dbc +SchedulerConfig.txt +scenicView.properties +ScenariumConfig.txt +*.class +*~ +*.bck +build.number +/extern/ +/out/ +/.settings/ +/junit/ +/target/ diff --git a/.project b/.project new file mode 100644 index 0000000..221dfab --- /dev/null +++ b/.project @@ -0,0 +1,24 @@ + + + atriasoft-png-decoder + + + atriasoft-png-decoder + + + + org.eclipse.jdt.core.javabuilder + + + + + net.sf.eclipsecs.core.CheckstyleBuilder + + + + + + org.eclipse.jdt.core.javanature + net.sf.eclipsecs.core.CheckstyleNature + + diff --git a/CheckStyle.xml b/CheckStyle.xml new file mode 100755 index 0000000..d68aedd --- /dev/null +++ b/CheckStyle.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CleanUp.xml b/CleanUp.xml new file mode 100644 index 0000000..543d7b0 --- /dev/null +++ b/CleanUp.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Formatter.xml b/Formatter.xml new file mode 100644 index 0000000..aec36fe --- /dev/null +++ b/Formatter.xml @@ -0,0 +1,390 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3211fa6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +BSD licence +=========== + +Copyright (c) 2008-2010, Matthias Mann + +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 Matthias Mann 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. \ No newline at end of file diff --git a/src/module-info.java b/src/module-info.java new file mode 100644 index 0000000..60abdaa --- /dev/null +++ b/src/module-info.java @@ -0,0 +1,10 @@ +/** Basic module interface. + * + * @author Edouard DUPIN */ + +open module org.atriasoft.pngdecoder { + exports org.atriasoft.pngdecoder; + + requires transitive org.atriasoft.egami; + requires transitive io.scenarium.logger; +} diff --git a/src/org/atriasoft/pngdecoder/PNGDecoder.java b/src/org/atriasoft/pngdecoder/PNGDecoder.java new file mode 100644 index 0000000..a8bf785 --- /dev/null +++ b/src/org/atriasoft/pngdecoder/PNGDecoder.java @@ -0,0 +1,896 @@ +/* + * Copyright (c) 2008-2010, Matthias Mann + * + * 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 Matthias Mann 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 org.atriasoft.pngdecoder; + +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; + +/** + * A PNGDecoder. The slick PNG decoder is based on this class :) + * + * @author Matthias Mann + */ +public class PNGDecoder { + + public enum Format { + ABGR(4, true), ALPHA(1, true), BGRA(4, true), LUMINANCE(1, false), LUMINANCE_ALPHA(2, true), RGB(3, false), RGBA(4, true); + + final boolean hasAlpha; + final int numComponents; + + 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 COLOR_GREYALPHA = 4; + + private static final byte COLOR_GREYSCALE = 0; + private static final byte COLOR_INDEXED = 3; + private static final byte COLOR_TRUEALPHA = 6; + private static final byte COLOR_TRUECOLOR = 2; + private static final int IDAT = 0x49444154; + + private static final int IEND = 0x49454E44; + private static final int IHDR = 0x49484452; + private static final int PLTE = 0x504C5445; + private static final byte[] SIGNATURE = { (byte) 137, 80, 78, 71, 13, 10, 26, 10 }; + private static final int T_RNS = 0x74524E53; + + private static boolean checkSignature(final byte[] buffer) { + for (int i = 0; i < PNGDecoder.SIGNATURE.length; i++) { + if (buffer[i] != PNGDecoder.SIGNATURE[i]) { + return false; + } + } + return true; + } + private int bitdepth; + private final byte[] buffer; + + private int bytesPerPixel; + private int chunkLength; + private int chunkRemaining; + + private int chunkType; + private int colorType; + private final CRC32 crc; + private int height; + private final InputStream input; + private byte[] palette; + private byte[] paletteA; + private byte[] transPixel; + + private int width; + + public PNGDecoder(final InputStream input) throws IOException { + this.input = input; + this.crc = new CRC32(); + this.buffer = new byte[4096]; + + readFully(this.buffer, 0, PNGDecoder.SIGNATURE.length); + if (!PNGDecoder.checkSignature(this.buffer)) { + throw new IOException("Not a valid PNG file"); + } + + openChunk(PNGDecoder.IHDR); + readIHDR(); + closeChunk(); + + searchIDAT: + for (;;) { + openChunk(); + switch (this.chunkType) { + default: + break; + case IDAT: + break searchIDAT; + case PLTE: + readPLTE(); + break; + case T_RNS: + readtRNS(); + break; + } + closeChunk(); + } + + if (this.colorType == PNGDecoder.COLOR_INDEXED && this.palette == null) { + throw new IOException("Missing PLTE chunk"); + } + } + + private void checkChunkLength(final int expected) throws IOException { + if (this.chunkLength != expected) { + throw new IOException("Chunk has wrong size"); + } + } + + 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); + int expectedCrc = readInt(this.buffer, 0); + 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 copy(final ByteBuffer buffer, final byte[] curLine) { + buffer.put(curLine, 1, curLine.length - 1); + } + + private void copyPALtoABGR(final ByteBuffer buffer, final byte[] curLine) { + if (this.paletteA != null) { + for (int i = 1, n = curLine.length; i < n; i += 1) { + int idx = curLine[i] & 255; + byte r = this.palette[idx * 3 + 0]; + byte g = this.palette[idx * 3 + 1]; + byte b = this.palette[idx * 3 + 2]; + 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) { + int idx = curLine[i] & 255; + byte r = this.palette[idx * 3 + 0]; + byte g = this.palette[idx * 3 + 1]; + byte b = this.palette[idx * 3 + 2]; + byte a = (byte) 0xFF; + buffer.put(a).put(b).put(g).put(r); + } + } + } + + private void copyPALtoBGRA(final ByteBuffer buffer, final byte[] curLine) { + if (this.paletteA != null) { + for (int i = 1, n = curLine.length; i < n; i += 1) { + int idx = curLine[i] & 255; + byte r = this.palette[idx * 3 + 0]; + byte g = this.palette[idx * 3 + 1]; + byte b = this.palette[idx * 3 + 2]; + 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) { + int idx = curLine[i] & 255; + byte r = this.palette[idx * 3 + 0]; + byte g = this.palette[idx * 3 + 1]; + byte b = this.palette[idx * 3 + 2]; + byte a = (byte) 0xFF; + buffer.put(b).put(g).put(r).put(a); + } + } + } + + private void copyPALtoRGBA(final ByteBuffer buffer, final byte[] curLine) { + if (this.paletteA != null) { + for (int i = 1, n = curLine.length; i < n; i += 1) { + int idx = curLine[i] & 255; + byte r = this.palette[idx * 3 + 0]; + byte g = this.palette[idx * 3 + 1]; + byte b = this.palette[idx * 3 + 2]; + 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) { + int idx = curLine[i] & 255; + byte r = this.palette[idx * 3 + 0]; + byte g = this.palette[idx * 3 + 1]; + byte b = this.palette[idx * 3 + 2]; + byte a = (byte) 0xFF; + buffer.put(r).put(g).put(b).put(a); + } + } + } + + 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 copyRGBtoABGR(final ByteBuffer buffer, final byte[] curLine) { + if (this.transPixel != null) { + byte tr = this.transPixel[1]; + byte tg = this.transPixel[3]; + byte tb = this.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 copyRGBtoBGRA(final ByteBuffer buffer, final byte[] curLine) { + if (this.transPixel != null) { + byte tr = this.transPixel[1]; + byte tg = this.transPixel[3]; + byte tb = this.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 copyRGBtoRGBA(final ByteBuffer buffer, final byte[] curLine) { + if (this.transPixel != null) { + byte tr = this.transPixel[1]; + byte tg = this.transPixel[3]; + byte tb = this.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); + } + } + } + + /** + * 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"); + } + + 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"); + } + int pos = buffer.position(); + int posDelta = (this.height - 1) * stride; + buffer.position(pos + posDelta); + decode(buffer, -stride, fmt); + buffer.position(buffer.position() + posDelta); + } + + private void expand1(final byte[] src, final 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 expand2(final byte[] src, final 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 expand4(final byte[] src, final 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); + } + } + } + + public int getHeight() { + return this.height; + } + + public int getWidth() { + return this.width; + } + + /** + * 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; + } + + /** + * 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 == PNGDecoder.COLOR_TRUEALPHA || this.colorType == PNGDecoder.COLOR_GREYALPHA; + } + + public boolean isRGB() { + return this.colorType == PNGDecoder.COLOR_TRUEALPHA || this.colorType == PNGDecoder.COLOR_TRUECOLOR || this.colorType == PNGDecoder.COLOR_INDEXED; + } + + 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)); + } + } + + /** + * Overwrites the tRNS chunk entry to make a selected color transparent. + *

This can only be invoked when the image has no alpha channel.

+ *

Calling this method causes {@link #hasAlpha()} to return true.

+ * + * @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"); + } + 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; + } + } + } + } + + 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 readChunkUnzip(final Inflater inflater, final byte[] buffer, int offset, int length) throws IOException { + assert (buffer != this.buffer); + try { + do { + int read = inflater.inflate(buffer, offset, length); + if (read <= 0) { + if (inflater.finished()) { + throw new EOFException(); + } + if (!inflater.needsInput()) { + throw new IOException("Can't inflate " + length + " bytes"); + } + refillInflater(inflater); + } else { + offset += read; + length -= read; + } + } while (length > 0); + } catch (DataFormatException ex) { + throw (IOException) (new IOException("inflate error").initCause(ex)); + } + } + + private void readFully(final byte[] buffer, int offset, int length) throws IOException { + do { + int read = this.input.read(buffer, offset, length); + if (read < 0) { + throw new EOFException(); + } + offset += read; + length -= read; + } while (length > 0); + } + + 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 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 readPLTE() throws IOException { + 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 refillInflater(final Inflater inflater) throws IOException { + while (this.chunkRemaining == 0) { + closeChunk(); + openChunk(PNGDecoder.IDAT); + } + int read = readChunk(this.buffer, 0, this.buffer.length); + inflater.setInput(this.buffer, 0, read); + } + + private void skip(long amount) throws IOException { + while (amount > 0) { + long skipped = this.input.skip(amount); + if (skipped < 0) { + throw new EOFException(); + } + amount -= skipped; + } + } + + 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 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 (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 (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 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) { + final int bpp = this.bytesPerPixel; + for (int i = 1, n = curLine.length; i < n; ++i) { + curLine[i] += prevLine[i]; + } + } +} diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..6c6aa7c --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +0.1.0 \ No newline at end of file