From 4304ef8f6ce7de58a04e98c0d967da697fb68fd0 Mon Sep 17 00:00:00 2001 From: Edouard DUPIN Date: Mon, 3 May 2021 16:34:15 +0200 Subject: [PATCH] [DEV] add basic png decoder and remove dependency with java.desktop --- .checkstyle | 7 + .classpath | 34 + .gitignore | 17 + .project | 24 + CheckStyle.xml | 66 ++ CleanUp.xml | 106 +++ Formatter.xml | 390 ++++++++ LICENSE | 30 + src/module-info.java | 10 + src/org/atriasoft/pngdecoder/PNGDecoder.java | 896 +++++++++++++++++++ version.txt | 1 + 11 files changed, 1581 insertions(+) create mode 100644 .checkstyle create mode 100644 .classpath create mode 100644 .gitignore create mode 100644 .project create mode 100755 CheckStyle.xml create mode 100644 CleanUp.xml create mode 100644 Formatter.xml create mode 100644 LICENSE create mode 100644 src/module-info.java create mode 100644 src/org/atriasoft/pngdecoder/PNGDecoder.java create mode 100644 version.txt 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