From 9a27f62dec42fec838726e5133a6ea47447a9d70 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 4 Sep 2013 13:23:17 +0200 Subject: [PATCH 01/98] Created replacement for StringBufferInputStream that properly encodes chars into bytes. --- .../twelvemonkeys/io/StringInputStream.java | 87 ++++++++++++ .../io/StringInputStreamTest.java | 126 ++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/StringInputStream.java create mode 100644 sandbox/sandbox-common/src/test/java/com/twelvemonkeys/io/StringInputStreamTest.java diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/StringInputStream.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/StringInputStream.java new file mode 100644 index 00000000..61e0fb50 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/StringInputStream.java @@ -0,0 +1,87 @@ +package com.twelvemonkeys.io; + +import com.twelvemonkeys.lang.Validate; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; + +/** + * An {@code InputStream} that reads bytes from a {@code String}. + * + * This class properly converts characters into bytes using a {@code Charset}, + * unlike the deprecated {@link java.io.StringBufferInputStream}. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: StringInputStream.java,v 1.0 03.09.13 10:19 haraldk Exp$ + */ +public final class StringInputStream extends InputStream { + + private final CharBuffer chars; + private final CharsetEncoder encoder; + private final ByteBuffer buffer; + + public StringInputStream(final String string, final Charset charset) { + this(Validate.notNull(string, "string"), 0, string.length(), charset); + } + + public StringInputStream(final String string, int offset, int length, final Charset charset) { + chars = CharBuffer.wrap(Validate.notNull(string, "string"), offset, offset + length); + encoder = Validate.notNull(charset, "charset").newEncoder(); + buffer = ByteBuffer.allocate(256); + buffer.flip(); + } + + private boolean fillBuffer() { + buffer.clear(); + encoder.encode(chars, buffer, chars.hasRemaining()); // TODO: Do we have to care about the result? + buffer.flip(); + + return buffer.hasRemaining(); + } + + private boolean ensureBuffer() { + return buffer.hasRemaining() || (chars.hasRemaining() && fillBuffer()); + } + + @Override + public int read() throws IOException { + if (!ensureBuffer()) { + return -1; + } + + return buffer.get() & 0xff; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (!ensureBuffer()) { + return -1; + } + + int count = Math.min(buffer.remaining(), len); + buffer.get(b, off, count); + return count; + } + + @Override + public long skip(long len) throws IOException { + if (!ensureBuffer()) { + return -1; + } + + int count = (int) Math.min(buffer.remaining(), len); + int position = buffer.position(); + buffer.position(position + count); + return count; + } + + @Override + public int available() throws IOException { + return buffer.remaining(); + } +} diff --git a/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/io/StringInputStreamTest.java b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/io/StringInputStreamTest.java new file mode 100644 index 00000000..d515e8bd --- /dev/null +++ b/sandbox/sandbox-common/src/test/java/com/twelvemonkeys/io/StringInputStreamTest.java @@ -0,0 +1,126 @@ +package com.twelvemonkeys.io; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Arrays; + +import static org.junit.Assert.*; + +/** + * StringInputStreamTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: StringInputStreamTest.java,v 1.0 03.09.13 10:40 haraldk Exp$ + */ +public class StringInputStreamTest { + + static final Charset UTF8 = Charset.forName("UTF-8"); + static final String LONG_STRING = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse id est lobortis, elementum nisi id, mollis urna. Morbi lorem nulla, vehicula ut ultricies ut, blandit sit amet metus. Praesent ut urna et arcu commodo tempus. Aenean dapibus commodo ligula, non vehicula leo dictum a. Aenean at leo ut eros hendrerit pellentesque. Phasellus sagittis arcu non faucibus faucibus. Sed volutpat vulputate metus sed consequat. Aenean auctor sapien sit amet erat dictum laoreet. Nullam libero felis, rutrum scelerisque elit eu, porta mollis nisi. Vestibulum vel ultricies turpis, vel dignissim arcu.\n" + + "Ut convallis erat et dapibus feugiat. Pellentesque eu dictum ligula, et interdum nibh. Sed rutrum justo a leo faucibus eleifend. Proin est justo, porttitor vel nulla egestas, faucibus scelerisque lacus. Vivamus sit amet gravida nibh. Praesent odio diam, ornare vitae mi nec, pretium ultrices tellus. Pellentesque vitae felis consequat mauris lacinia condimentum in ut nibh. In odio quam, laoreet luctus velit vel, suscipit mollis leo. Etiam justo nulla, posuere et massa non, pretium vehicula diam. Sed porta molestie mauris quis condimentum. Sed quis gravida ipsum, eget porttitor felis. Vivamus volutpat velit vitae dolor convallis, nec malesuada est porttitor. Proin sed purus vel leo pretium suscipit. Morbi ut nibh quis tortor vehicula porttitor non sit amet lorem. Proin tempor vel sem sit amet accumsan.\n" + + "Cras vulputate orci a lorem luctus, vel egestas leo porttitor. Duis venenatis odio et mauris molestie rutrum. Mauris gravida volutpat odio at consequat. Mauris eros purus, bibendum in vulputate vitae, laoreet quis libero. Quisque lacinia, neque sed semper fringilla, elit dolor sagittis est, nec tincidunt ipsum risus ut sem. Maecenas consectetur aliquam augue. Etiam neque mi, euismod eget metus quis, molestie lacinia odio. Sed eget sollicitudin metus. Phasellus facilisis augue et sem facilisis, consequat mollis augue ultricies.\n" + + "Vivamus in porta massa. Sed eget lorem non lectus viverra pretium. Curabitur convallis posuere est vestibulum vulputate. Maecenas placerat risus ut dui hendrerit, sed suscipit magna tincidunt. Etiam ut mattis dolor, quis dictum velit. Donec ut dui sit amet libero convallis euismod. Phasellus dapibus dolor in nibh volutpat, eu scelerisque neque tempus. Maecenas a rhoncus velit. Etiam sollicitudin, leo non euismod vehicula, lectus risus aliquet metus, quis cursus purus orci non turpis. Nulla vel enim tortor. Quisque nec mi vulputate, convallis orci vel, suscipit nibh. Sed sed tellus id elit commodo laoreet ut euismod ligula. Mauris suscipit commodo interdum. Phasellus scelerisque arcu nec nibh porta, et semper massa rutrum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.\n" + + "Praesent cursus, sapien ut venenatis malesuada, turpis nulla venenatis velit, nec tristique leo turpis auctor purus. Curabitur non porta urna. Sed vitae felis massa. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Phasellus scelerisque id dolor nec fermentum. Etiam suscipit tincidunt odio, sed molestie elit fringilla in. Phasellus nec euismod lacus. Suspendisse bibendum vulputate viverra. Fusce mollis pharetra imperdiet. Phasellus tortor eros, rhoncus volutpat diam in, scelerisque viverra felis. Ut ornare urna commodo, pretium mauris eget, eleifend ipsum."; + static final String SHORT_STRING = "Java"; + + @Test + public void testReadShortString() throws IOException { + StringInputStream stream = new StringInputStream(SHORT_STRING, UTF8); + + byte[] value = SHORT_STRING.getBytes(UTF8); + for (int i = 0; i < value.length; i++) { + int read = stream.read(); + assertEquals(String.format("Wrong value at offset %s: '%s' != '%s'", i, String.valueOf((char) value[i]), String.valueOf((char) (byte) read)), value[i] &0xff, read); + } + + assertEquals(-1, stream.read()); + } + + @Test + public void testReadSubString() throws IOException { + StringInputStream stream = new StringInputStream("foo bar xyzzy", 4, 3, UTF8); + + byte[] value = "bar".getBytes(UTF8); + for (int i = 0; i < value.length; i++) { + int read = stream.read(); + assertEquals(String.format("Wrong value at offset %s: '%s' != '%s'", i, String.valueOf((char) value[i]), String.valueOf((char) (byte) read)), value[i] &0xff, read); + } + + assertEquals(-1, stream.read()); + } + + @Test + public void testReadNonAsciiString() throws IOException { + String string = "\u00c6\u00d8\u00c5\u00e6\u00f8\u00e5\u00e1\u00e9\u00c0\u00c8\u00fc\u00dc\u00df"; + StringInputStream stream = new StringInputStream(string, UTF8); + + byte[] value = string.getBytes(UTF8); + for (int i = 0; i < value.length; i++) { + int read = stream.read(); + assertEquals(String.format("Wrong value at offset %s: '%s' != '%s'", i, String.valueOf((char) value[i]), String.valueOf((char) (byte) read)), value[i] &0xff, read); + } + + assertEquals(-1, stream.read()); + } + + @Test + public void testReadLongString() throws IOException { + StringInputStream stream = new StringInputStream(LONG_STRING, UTF8); + + byte[] value = LONG_STRING.getBytes(UTF8); + for (int i = 0; i < value.length; i++) { + int read = stream.read(); + assertEquals(String.format("Wrong value at offset %s: '%s' != '%s'", i, String.valueOf((char) value[i]), String.valueOf((char) (byte) read)), value[i] &0xff, read); + } + + assertEquals(-1, stream.read()); + } + + @Test + public void testReadArrayLongString() throws IOException { + StringInputStream stream = new StringInputStream(LONG_STRING, UTF8); + + byte[] value = LONG_STRING.getBytes(UTF8); + byte[] buffer = new byte[17]; + int count; + for (int i = 0; i < value.length; i += count) { + count = stream.read(buffer); + assertArrayEquals(String.format("Wrong value at offset %s", i), Arrays.copyOfRange(value, i, i + count), Arrays.copyOfRange(buffer, 0, count)); + } + + assertEquals(-1, stream.read()); + } + + @Test + public void testReadArraySkipLongString() throws IOException { + StringInputStream stream = new StringInputStream(LONG_STRING, UTF8); + + byte[] value = LONG_STRING.getBytes(UTF8); + byte[] buffer = new byte[17]; + int count; + for (int i = 0; i < value.length; i += count) { + if (i % 2 == 0) { + count = (int) stream.skip(buffer.length); + } + else { + count = stream.read(buffer); + assertArrayEquals(String.format("Wrong value at offset %s", i), Arrays.copyOfRange(value, i, i + count), Arrays.copyOfRange(buffer, 0, count)); + } + } + + assertEquals(-1, stream.read()); + } + + /*@Test + public */void testPerformance() throws IOException { + for (int i = 0; i < 100000; i++) { + StringInputStream stream = new StringInputStream(LONG_STRING, UTF8); + while(stream.read() != -1) { + stream.available(); + } + } + + } +} From f2ff00580a5d77d85f52fc122b301d5edb1376f0 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sun, 8 Sep 2013 13:39:13 +0200 Subject: [PATCH 02/98] TMC-IOENC: Refactored Decoder to use ByteBuffer instead of byte[] for better readability/simpler code. --- .../io/enc/AbstractRLEDecoder.java | 18 ++--- .../twelvemonkeys/io/enc/Base64Decoder.java | 43 ++++------ .../com/twelvemonkeys/io/enc/Decoder.java | 5 +- .../twelvemonkeys/io/enc/DecoderStream.java | 81 ++++++++----------- .../io/enc/PackBits16Decoder.java | 19 ++--- .../twelvemonkeys/io/enc/PackBitsDecoder.java | 26 +++--- .../io/enc/DecoderAbstractTestCase.java | 3 +- .../imageio/plugins/tiff/LZWDecoder.java | 28 +++---- .../twelvemonkeys/io/enc/InflateDecoder.java | 5 +- 9 files changed, 102 insertions(+), 126 deletions(-) diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java index 661a2dcf..9a427220 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java @@ -31,6 +31,7 @@ package com.twelvemonkeys.io.enc; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; /** * Abstract base class for RLE decoding as specified by in the Windows BMP (aka DIB) file format. @@ -86,25 +87,24 @@ abstract class AbstractRLEDecoder implements Decoder { * Decodes as much data as possible, from the stream into the buffer. * * @param pStream the input stream containing RLE data - * @param pBuffer tge buffer to decode the data to + * @param pBuffer the buffer to decode the data to * * @return the number of bytes decoded from the stream, to the buffer * * @throws IOException if an I/O related exception ocurs while reading */ - public final int decode(InputStream pStream, byte[] pBuffer) throws IOException { - int decoded = 0; - - while (decoded < pBuffer.length && dstY >= 0) { + public final int decode(InputStream pStream, ByteBuffer pBuffer) throws IOException { + while (pBuffer.hasRemaining() && dstY >= 0) { // NOTE: Decode only full rows, don't decode if y delta if (dstX == 0 && srcY == dstY) { decodeRow(pStream); } - int length = Math.min(row.length - dstX, pBuffer.length - decoded); - System.arraycopy(row, dstX, pBuffer, decoded, length); + int length = Math.min(row.length - dstX, pBuffer.remaining()); +// System.arraycopy(row, dstX, pBuffer, decoded, length); + pBuffer.put(row, 0, length); dstX += length; - decoded += length; +// decoded += length; if (dstX == row.length) { dstX = 0; @@ -120,7 +120,7 @@ abstract class AbstractRLEDecoder implements Decoder { } } - return decoded; + return pBuffer.position(); } /** diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java index 38179949..dc9f319a 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java @@ -28,9 +28,9 @@ package com.twelvemonkeys.io.enc; -import com.twelvemonkeys.io.FastByteArrayOutputStream; - -import java.io.*; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; /** * {@code Decoder} implementation for standard base64 encoding. @@ -62,8 +62,6 @@ public final class Base64Decoder implements Decoder { final static byte[] PEM_CONVERT_ARRAY; private byte[] decodeBuffer = new byte[4]; - private ByteArrayOutputStream wrapped; - private Object wrappedObject; static { PEM_CONVERT_ARRAY = new byte[256]; @@ -93,7 +91,7 @@ public final class Base64Decoder implements Decoder { return pLength; } - protected boolean decodeAtom(final InputStream pInput, final OutputStream pOutput, final int pLength) + protected boolean decodeAtom(final InputStream pInput, final ByteBuffer pOutput, final int pLength) throws IOException { byte byte0 = -1; @@ -147,16 +145,16 @@ public final class Base64Decoder implements Decoder { default: switch (length) { case 2: - pOutput.write((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3)); + pOutput.put((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3)); break; case 3: - pOutput.write((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3)); - pOutput.write((byte) (byte1 << 4 & 240 | byte2 >>> 2 & 15)); + pOutput.put((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3)); + pOutput.put((byte) (byte1 << 4 & 240 | byte2 >>> 2 & 15)); break; case 4: - pOutput.write((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3)); - pOutput.write((byte) (byte1 << 4 & 240 | byte2 >>> 2 & 15)); - pOutput.write((byte) (byte2 << 6 & 192 | byte3 & 63)); + pOutput.put((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3)); + pOutput.put((byte) (byte1 << 4 & 240 | byte2 >>> 2 & 15)); + pOutput.put((byte) (byte2 << 6 & 192 | byte3 & 63)); break; } @@ -166,34 +164,23 @@ public final class Base64Decoder implements Decoder { return true; } - void decodeBuffer(final InputStream pInput, final ByteArrayOutputStream pOutput, final int pLength) throws IOException { + public int decode(final InputStream pStream, final ByteBuffer pBuffer) throws IOException { do { int k = 72; int i; for (i = 0; i + 4 < k; i += 4) { - if(!decodeAtom(pInput, pOutput, 4)) { + if(!decodeAtom(pStream, pBuffer, 4)) { break; } } - if (!decodeAtom(pInput, pOutput, k - i)) { + if (!decodeAtom(pStream, pBuffer, k - i)) { break; } } - while (pOutput.size() + 54 < pLength); // 72 char lines should produce no more than 54 bytes - } + while (pBuffer.remaining() > 54); // 72 char lines should produce no more than 54 bytes - public int decode(final InputStream pStream, final byte[] pBuffer) throws IOException { - if (wrappedObject != pBuffer) { - // NOTE: Array not cloned in FastByteArrayOutputStream - wrapped = new FastByteArrayOutputStream(pBuffer); - wrappedObject = pBuffer; - } - - wrapped.reset(); // NOTE: This only resets count to 0 - decodeBuffer(pStream, wrapped, pBuffer.length); - - return wrapped.size(); + return pBuffer.position(); } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Decoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Decoder.java index 45219c91..0ceda346 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Decoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Decoder.java @@ -28,8 +28,9 @@ package com.twelvemonkeys.io.enc; -import java.io.InputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; /** * Interface for decoders. @@ -60,5 +61,5 @@ public interface Decoder { * @throws IOException if an I/O error occurs * @throws java.io.EOFException if a premature end-of-file is encountered */ - int decode(InputStream pStream, byte[] pBuffer) throws IOException; + int decode(InputStream pStream, ByteBuffer pBuffer) throws IOException; } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java index 318ccb86..9a36ed15 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java @@ -28,9 +28,10 @@ package com.twelvemonkeys.io.enc; -import java.io.InputStream; -import java.io.IOException; import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; /** * An {@code InputStream} that provides on-the-fly decoding from an underlying @@ -43,12 +44,12 @@ import java.io.FilterInputStream; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java#2 $ */ public final class DecoderStream extends FilterInputStream { - - protected int bufferPos; - protected int bufferLimit; - protected final byte[] buffer; + protected final ByteBuffer buffer; protected final Decoder decoder; + // TODO: Consider replacing the wrapped input stream with a channel like this + // ReadableByteChannel inChannel = Channels.newChannel(stream); + /** * Creates a new decoder stream and chains it to the * input stream specified by the {@code pStream} argument. @@ -77,29 +78,22 @@ public final class DecoderStream extends FilterInputStream { public DecoderStream(final InputStream pStream, final Decoder pDecoder, final int pBufferSize) { super(pStream); decoder = pDecoder; - buffer = new byte[pBufferSize]; - bufferPos = 0; - bufferLimit = 0; + buffer = ByteBuffer.allocate(pBufferSize); + buffer.flip(); } public int available() throws IOException { - return bufferLimit - bufferPos + super.available(); + return buffer.remaining(); } public int read() throws IOException { - if (bufferPos == bufferLimit) { - bufferLimit = fill(); + if (!buffer.hasRemaining()) { + if (fill() < 0) { + return -1; + } } - if (bufferLimit < 0) { - return -1; - } - - return buffer[bufferPos++] & 0xff; - } - - public int read(final byte pBytes[]) throws IOException { - return read(pBytes, 0, pBytes.length); + return buffer.get() & 0xff; } public int read(final byte pBytes[], final int pOffset, final int pLength) throws IOException { @@ -115,8 +109,10 @@ public final class DecoderStream extends FilterInputStream { } // End of file? - if ((bufferLimit - bufferPos) < 0) { - return -1; + if (!buffer.hasRemaining()) { + if (fill() < 0) { + return -1; + } } // Read until we have read pLength bytes, or have reached EOF @@ -124,21 +120,15 @@ public final class DecoderStream extends FilterInputStream { int off = pOffset; while (pLength > count) { - int avail = bufferLimit - bufferPos; - - if (avail <= 0) { - bufferLimit = fill(); - - if (bufferLimit < 0) { + if (!buffer.hasRemaining()) { + if (fill() < 0) { break; } } // Copy as many bytes as possible - int dstLen = Math.min(pLength - count, avail); - System.arraycopy(buffer, bufferPos, pBytes, off, dstLen); - - bufferPos += dstLen; + int dstLen = Math.min(pLength - count, buffer.remaining()); + buffer.get(pBytes, off, dstLen); // Update offset (rest) off += dstLen; @@ -152,29 +142,25 @@ public final class DecoderStream extends FilterInputStream { public long skip(final long pLength) throws IOException { // End of file? - if (bufferLimit - bufferPos < 0) { - return 0; + if (!buffer.hasRemaining()) { + if (fill() < 0) { + return 0; // Yes, 0, not -1 + } } // Skip until we have skipped pLength bytes, or have reached EOF long total = 0; while (total < pLength) { - int avail = bufferLimit - bufferPos; - - if (avail == 0) { - bufferLimit = fill(); - - if (bufferLimit < 0) { + if (!buffer.hasRemaining()) { + if (fill() < 0) { break; } } // NOTE: Skipped can never be more than avail, which is // an int, so the cast is safe - int skipped = (int) Math.min(pLength - total, avail); - - bufferPos += skipped; // Just skip these bytes + int skipped = (int) Math.min(pLength - total, buffer.remaining()); total += skipped; } @@ -190,19 +176,20 @@ public final class DecoderStream extends FilterInputStream { * @throws IOException if an I/O error occurs */ protected int fill() throws IOException { + buffer.clear(); int read = decoder.decode(in, buffer); // TODO: Enforce this in test case, leave here to aid debugging - if (read > buffer.length) { + if (read > buffer.capacity()) { throw new AssertionError( String.format( "Decode beyond buffer (%d): %d (using %s decoder)", - buffer.length, read, decoder.getClass().getName() + buffer.capacity(), read, decoder.getClass().getName() ) ); } - bufferPos = 0; + buffer.flip(); if (read == 0) { return -1; diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java index a7b1dde0..a141d30f 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java @@ -28,9 +28,10 @@ package com.twelvemonkeys.io.enc; -import java.io.InputStream; -import java.io.IOException; import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; /** * Decoder implementation for 16 bit-chunked Apple PackBits-like run-length @@ -84,13 +85,13 @@ public final class PackBits16Decoder implements Decoder { * * @throws java.io.IOException */ - public int decode(final InputStream pStream, final byte[] pBuffer) throws IOException { + public int decode(final InputStream pStream, final ByteBuffer pBuffer) throws IOException { if (reachedEOF) { return -1; } int read = 0; - final int max = pBuffer.length; + final int max = pBuffer.capacity(); while (read < max) { int n; @@ -126,7 +127,7 @@ public final class PackBits16Decoder implements Decoder { if (n >= 0) { // Copy next n + 1 shorts literally int len = 2 * (n + 1); - readFully(pStream, pBuffer, read, len); + readFully(pStream, pBuffer, len); read += len; } // Allow -128 for compatibility, see above @@ -136,8 +137,8 @@ public final class PackBits16Decoder implements Decoder { byte value2 = readByte(pStream); for (int i = -n + 1; i > 0; i--) { - pBuffer[read++] = value1; - pBuffer[read++] = value2; + pBuffer.put(value1); + pBuffer.put(value2); } } // else NOOP (-128) @@ -160,7 +161,7 @@ public final class PackBits16Decoder implements Decoder { return (byte) read; } - private static void readFully(final InputStream pStream, final byte[] pBuffer, final int pOffset, final int pLength) throws IOException { + private static void readFully(final InputStream pStream, final ByteBuffer pBuffer, final int pLength) throws IOException { if (pLength < 0) { throw new IndexOutOfBoundsException(); } @@ -168,7 +169,7 @@ public final class PackBits16Decoder implements Decoder { int read = 0; while (read < pLength) { - int count = pStream.read(pBuffer, pOffset + read, pLength - read); + int count = pStream.read(pBuffer.array(), pBuffer.arrayOffset() + pBuffer.position() + read, pLength - read); if (count < 0) { throw new EOFException("Unexpected end of PackBits stream"); diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java index 4140f298..9cb9ad73 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java @@ -31,6 +31,7 @@ package com.twelvemonkeys.io.enc; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; /** * Decoder implementation for Apple PackBits run-length encoding. @@ -98,16 +99,13 @@ public final class PackBitsDecoder implements Decoder { * * @throws java.io.IOException */ - public int decode(final InputStream pStream, final byte[] pBuffer) throws IOException { + public int decode(final InputStream pStream, final ByteBuffer pBuffer) throws IOException { if (reachedEOF) { return -1; } - int read = 0; - final int max = pBuffer.length; - // TODO: Don't decode more than single runs, because some writers add pad bytes inside the stream... - while (read < max) { + while (pBuffer.hasRemaining()) { int n; if (splitRun) { @@ -126,12 +124,12 @@ public final class PackBitsDecoder implements Decoder { } // Split run at or before max - if (n >= 0 && n + 1 + read > max) { + if (n >= 0 && n + 1 > pBuffer.remaining()) { leftOfRun = n; splitRun = true; break; } - else if (n < 0 && -n + 1 + read > max) { + else if (n < 0 && -n + 1 > pBuffer.remaining()) { leftOfRun = n; splitRun = true; break; @@ -140,9 +138,7 @@ public final class PackBitsDecoder implements Decoder { try { if (n >= 0) { // Copy next n + 1 bytes literally - readFully(pStream, pBuffer, read, n + 1); - - read += n + 1; + readFully(pStream, pBuffer, n + 1); } // Allow -128 for compatibility, see above else if (disableNoop || n != -128) { @@ -150,7 +146,7 @@ public final class PackBitsDecoder implements Decoder { byte value = readByte(pStream); for (int i = -n + 1; i > 0; i--) { - pBuffer[read++] = value; + pBuffer.put(value); } } // else NOOP (-128) @@ -160,7 +156,7 @@ public final class PackBitsDecoder implements Decoder { } } - return read; + return pBuffer.position(); } static byte readByte(final InputStream pStream) throws IOException { @@ -173,7 +169,7 @@ public final class PackBitsDecoder implements Decoder { return (byte) read; } - static void readFully(final InputStream pStream, final byte[] pBuffer, final int pOffset, final int pLength) throws IOException { + static void readFully(final InputStream pStream, final ByteBuffer pBuffer, final int pLength) throws IOException { if (pLength < 0) { throw new IndexOutOfBoundsException(String.format("Negative length: %d", pLength)); } @@ -181,7 +177,7 @@ public final class PackBitsDecoder implements Decoder { int total = 0; while (total < pLength) { - int count = pStream.read(pBuffer, pOffset + total, pLength - total); + int count = pStream.read(pBuffer.array(), pBuffer.arrayOffset() + pBuffer.position() + total, pLength - total); if (count < 0) { throw new EOFException("Unexpected end of PackBits stream"); @@ -189,5 +185,7 @@ public final class PackBitsDecoder implements Decoder { total += count; } + + pBuffer.position(pBuffer.position() + total); } } diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/DecoderAbstractTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/DecoderAbstractTestCase.java index 70dcfa04..51788010 100644 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/DecoderAbstractTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/DecoderAbstractTestCase.java @@ -5,6 +5,7 @@ import com.twelvemonkeys.lang.ObjectAbstractTestCase; import org.junit.Test; import java.io.*; +import java.nio.ByteBuffer; import static org.junit.Assert.*; @@ -39,7 +40,7 @@ public abstract class DecoderAbstractTestCase extends ObjectAbstractTestCase { ByteArrayInputStream bytes = new ByteArrayInputStream(new byte[0]); try { - int count = decoder.decode(bytes, new byte[128]); + int count = decoder.decode(bytes, ByteBuffer.allocate(128)); assertEquals("Should not be able to read any bytes", 0, count); } catch (EOFException allowed) { diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java index bf573fa9..789190d6 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java @@ -33,6 +33,7 @@ import com.twelvemonkeys.io.enc.Decoder; import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; /** * Lempel–Ziv–Welch (LZW) decompression. LZW is a universal loss-less data compression algorithm @@ -94,10 +95,9 @@ abstract class LZWDecoder implements Decoder { maxString = 1; } - public int decode(final InputStream stream, final byte[] buffer) throws IOException { + public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException { // Adapted from the pseudo-code example found in the TIFF 6.0 Specification, 1992. // See Section 13: "LZW Compression"/"LZW Decoding", page 61+ - int bufferPos = 0; int code; while ((code = getNextCode(stream)) != EOI_CODE) { @@ -109,30 +109,30 @@ abstract class LZWDecoder implements Decoder { break; } - bufferPos += table[code].writeTo(buffer, bufferPos); + table[code].writeTo(buffer); } else { if (isInTable(code)) { - bufferPos += table[code].writeTo(buffer, bufferPos); + table[code].writeTo(buffer); addStringToTable(table[oldCode].concatenate(table[code].firstChar)); } else { String outString = table[oldCode].concatenate(table[oldCode].firstChar); - bufferPos += outString.writeTo(buffer, bufferPos); + outString.writeTo(buffer); addStringToTable(outString); } } oldCode = code; - if (bufferPos >= buffer.length - maxString - 1) { + if (buffer.remaining() < maxString + 1) { // Buffer full, stop decoding for now break; } } - return bufferPos; + return buffer.position(); } private void addStringToTable(final String string) throws IOException { @@ -301,24 +301,24 @@ abstract class LZWDecoder implements Decoder { return new String(firstChar, this.firstChar, length + 1, this); } - public final int writeTo(final byte[] buffer, final int offset) { + public final void writeTo(final ByteBuffer buffer) { if (length == 0) { - return 0; + return; } - else if (length == 1) { - buffer[offset] = value; - return 1; + if (length == 1) { + buffer.put(value); } else { String e = this; + final int offset = buffer.position(); for (int i = length - 1; i >= 0; i--) { - buffer[offset + i] = e.value; + buffer.put(offset + i, e.value); e = e.previous; } - return length; + buffer.position(offset + length); } } } diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java index 78d26ee0..15c9c7a1 100644 --- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java @@ -31,6 +31,7 @@ package com.twelvemonkeys.io.enc; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; import java.util.zip.DataFormatException; import java.util.zip.Inflater; @@ -75,11 +76,11 @@ final class InflateDecoder implements Decoder { buffer = new byte[1024]; } - public int decode(final InputStream pStream, final byte[] pBuffer) throws IOException { + public int decode(final InputStream pStream, final ByteBuffer pBuffer) throws IOException { try { int decoded; - while ((decoded = inflater.inflate(pBuffer, 0, pBuffer.length)) == 0) { + while ((decoded = inflater.inflate(pBuffer.array(), pBuffer.arrayOffset(), pBuffer.capacity())) == 0) { if (inflater.finished() || inflater.needsDictionary()) { return 0; } From 55b161b1154a4f9bdbb126bc7c4eabc0a6eaf54c Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sun, 8 Sep 2013 13:41:16 +0200 Subject: [PATCH 03/98] TMC-IMAGE: Removed support for prehistoric JREs. --- .../com/twelvemonkeys/image/ResampleOp.java | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/ResampleOp.java b/common/common-image/src/main/java/com/twelvemonkeys/image/ResampleOp.java index 90f04cef..c2132eed 100644 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/ResampleOp.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/ResampleOp.java @@ -52,8 +52,6 @@ package com.twelvemonkeys.image; -import com.twelvemonkeys.lang.SystemUtil; - import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; @@ -294,7 +292,6 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { int height; int filterType; - private static final boolean TRANSFORM_OP_BICUBIC_SUPPORT = SystemUtil.isFieldAvailable(AffineTransformOp.class.getName(), "TYPE_BICUBIC"); /** * RendereingHints.Key implementation, works only with Value values. @@ -320,7 +317,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { } /** - * RenderingHints value implementaion, works with Key keys. + * RenderingHints value implementation, works with Key keys. */ // TODO: Extract abstract Value class, and move to AbstractBufferedImageOp static final class Value { @@ -331,8 +328,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { public Value(final RenderingHints.Key pKey, final String pName, final int pType) { key = pKey; name = pName; - validateFilterType(pType); - type = pType;// TODO: test for duplicates + type = validateFilterType(pType); } public boolean isCompatibleKey(Key pKey) { @@ -422,11 +418,10 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { this.width = width; this.height = height; - validateFilterType(filterType); - this.filterType = filterType; + this.filterType = validateFilterType(filterType); } - private static void validateFilterType(int pFilterType) { + private static int validateFilterType(int pFilterType) { switch (pFilterType) { case FILTER_UNDEFINED: case FILTER_POINT: @@ -444,7 +439,7 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { case FILTER_LANCZOS: case FILTER_BLACKMAN_BESSEL: case FILTER_BLACKMAN_SINC: - break; + return pFilterType; default: throw new IllegalArgumentException("Unknown filter type: " + pFilterType); } @@ -529,8 +524,8 @@ public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { } // Else fall through case FILTER_QUADRATIC: - if (input.getType() != BufferedImage.TYPE_CUSTOM && TRANSFORM_OP_BICUBIC_SUPPORT) { - return fastResample(input, output, width, height, 3); // AffineTransformOp.TYPE_BICUBIC + if (input.getType() != BufferedImage.TYPE_CUSTOM) { + return fastResample(input, output, width, height, AffineTransformOp.TYPE_BICUBIC); } // Else fall through default: From 4c18c2a68538cfd09f7374d43e6a27dded58ec0b Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sun, 8 Sep 2013 13:53:01 +0200 Subject: [PATCH 04/98] TMC-IMAGE: Added TODO, should probably retire class and move to sandbox --- .../java/com/twelvemonkeys/image/BrightnessContrastFilter.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java b/common/common-image/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java index 2b4fb736..0dbb7598 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java @@ -60,6 +60,8 @@ import java.awt.image.RGBImageFilter; public class BrightnessContrastFilter extends RGBImageFilter { + // TODO: Replace with RescaleOp? + // This filter can filter IndexColorModel, as it is does not depend on // the pixels' location { From c5f1d8101b3ddd95d8f2123a2692089754a6fc3c Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sun, 8 Sep 2013 14:08:38 +0200 Subject: [PATCH 05/98] Moved old obsolete stuff to sandbox. --- .../java/com/twelvemonkeys/net/HTTPUtil.java | 202 ++ .../com/twelvemonkeys/net/HTTPUtilTest.java | 83 + .../twelvemonkeys/net/NetUtilTestCase.java | 59 - .../net/AuthenticatorFilter.java | 91 +- .../java/com/twelvemonkeys/net/BASE64.java | 285 +- .../twelvemonkeys/net/HttpURLConnection.java | 2203 +++++++------- .../java/com/twelvemonkeys/net/NetUtil.java | 2678 ++++++++--------- .../net/PasswordAuthenticator.java | 90 +- .../net/SimpleAuthenticator.java | 540 ++-- .../servlet/cache/CacheResponseWrapper.java | 6 +- .../servlet/cache/HTTPCache.java | 16 +- .../cache/WritableCachedResponseImpl.java | 4 +- .../servlet/cache/HTTPCacheTestCase.java | 6 +- 13 files changed, 3161 insertions(+), 3102 deletions(-) create mode 100644 common/common-io/src/main/java/com/twelvemonkeys/net/HTTPUtil.java create mode 100644 common/common-io/src/test/java/com/twelvemonkeys/net/HTTPUtilTest.java delete mode 100755 common/common-io/src/test/java/com/twelvemonkeys/net/NetUtilTestCase.java rename {common/common-io => sandbox/sandbox-common}/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java (97%) rename {common/common-io => sandbox/sandbox-common}/src/main/java/com/twelvemonkeys/net/BASE64.java (97%) rename {common/common-io => sandbox/sandbox-common}/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java (97%) rename {common/common-io => sandbox/sandbox-common}/src/main/java/com/twelvemonkeys/net/NetUtil.java (85%) rename {common/common-io => sandbox/sandbox-common}/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java (95%) rename {common/common-io => sandbox/sandbox-common}/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java (97%) diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/HTTPUtil.java b/common/common-io/src/main/java/com/twelvemonkeys/net/HTTPUtil.java new file mode 100644 index 00000000..ba670646 --- /dev/null +++ b/common/common-io/src/main/java/com/twelvemonkeys/net/HTTPUtil.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2013, Harald Kuhr + * 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 "TwelveMonkeys" 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 com.twelvemonkeys.net; + +import com.twelvemonkeys.lang.DateUtil; +import com.twelvemonkeys.lang.StringUtil; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + * HTTPUtil + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: HTTPUtil.java,v 1.0 08.09.13 13:57 haraldk Exp$ + */ +public class HTTPUtil { + /** + * RFC 1123 date format, as recommended by RFC 2616 (HTTP/1.1), sec 3.3 + * NOTE: All date formats are private, to ensure synchronized access. + */ + private static final SimpleDateFormat HTTP_RFC1123_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + static { + HTTP_RFC1123_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); + } + + /** + * RFC 850 date format, (almost) as described in RFC 2616 (HTTP/1.1), sec 3.3 + * USE FOR PARSING ONLY (format is not 100% correct, to be more robust). + */ + private static final SimpleDateFormat HTTP_RFC850_FORMAT = new SimpleDateFormat("EEE, dd-MMM-yy HH:mm:ss z", Locale.US); + /** + * ANSI C asctime() date format, (almost) as described in RFC 2616 (HTTP/1.1), sec 3.3. + * USE FOR PARSING ONLY (format is not 100% correct, to be more robust). + */ + private static final SimpleDateFormat HTTP_ASCTIME_FORMAT = new SimpleDateFormat("EEE MMM d HH:mm:ss yy", Locale.US); + + private static long sNext50YearWindowChange = DateUtil.currentTimeDay(); + static { + HTTP_RFC850_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); + HTTP_ASCTIME_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); + + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.3: + // - HTTP/1.1 clients and caches SHOULD assume that an RFC-850 date + // which appears to be more than 50 years in the future is in fact + // in the past (this helps solve the "year 2000" problem). + update50YearWindowIfNeeded(); + } + + private static void update50YearWindowIfNeeded() { + // Avoid class synchronization + long next = sNext50YearWindowChange; + + if (next < System.currentTimeMillis()) { + // Next check in one day + next += DateUtil.DAY; + sNext50YearWindowChange = next; + + Date startDate = new Date(next - (50l * DateUtil.CALENDAR_YEAR)); + //System.out.println("next test: " + new Date(next) + ", 50 year start: " + startDate); + synchronized (HTTP_RFC850_FORMAT) { + HTTP_RFC850_FORMAT.set2DigitYearStart(startDate); + } + synchronized (HTTP_ASCTIME_FORMAT) { + HTTP_ASCTIME_FORMAT.set2DigitYearStart(startDate); + } + } + } + + /** + * Formats the time to a HTTP date, using the RFC 1123 format, as described + * in RFC 2616 (HTTP/1.1), sec. 3.3. + * + * @param pTime the time + * @return a {@code String} representation of the time + */ + public static String formatHTTPDate(long pTime) { + return formatHTTPDate(new Date(pTime)); + } + + /** + * Formats the time to a HTTP date, using the RFC 1123 format, as described + * in RFC 2616 (HTTP/1.1), sec. 3.3. + * + * @param pTime the time + * @return a {@code String} representation of the time + */ + public static String formatHTTPDate(Date pTime) { + synchronized (HTTP_RFC1123_FORMAT) { + return HTTP_RFC1123_FORMAT.format(pTime); + } + } + + /** + * Parses a HTTP date string into a {@code long} representing milliseconds + * since January 1, 1970 GMT. + *

+ * Use this method with headers that contain dates, such as + * {@code If-Modified-Since} or {@code Last-Modified}. + *

+ * The date string may be in either RFC 1123, RFC 850 or ANSI C asctime() + * format, as described in + * RFC 2616 (HTTP/1.1), sec. 3.3 + * + * @param pDate the date to parse + * + * @return a {@code long} value representing the date, expressed as the + * number of milliseconds since January 1, 1970 GMT, + * @throws NumberFormatException if the date parameter is not parseable. + * @throws IllegalArgumentException if the date paramter is {@code null} + */ + public static long parseHTTPDate(String pDate) throws NumberFormatException { + return parseHTTPDateImpl(pDate).getTime(); + } + + /** + * ParseHTTPDate implementation + * + * @param pDate the date string to parse + * + * @return a {@code Date} + * @throws NumberFormatException if the date parameter is not parseable. + * @throws IllegalArgumentException if the date paramter is {@code null} + */ + private static Date parseHTTPDateImpl(final String pDate) throws NumberFormatException { + if (pDate == null) { + throw new IllegalArgumentException("date == null"); + } + + if (StringUtil.isEmpty(pDate)) { + throw new NumberFormatException("Invalid HTTP date: \"" + pDate + "\""); + } + + DateFormat format; + + if (pDate.indexOf('-') >= 0) { + format = HTTP_RFC850_FORMAT; + update50YearWindowIfNeeded(); + } + else if (pDate.indexOf(',') < 0) { + format = HTTP_ASCTIME_FORMAT; + update50YearWindowIfNeeded(); + } + else { + format = HTTP_RFC1123_FORMAT; + // NOTE: RFC1123 always uses 4-digit years + } + + Date date; + try { + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (format) { + date = format.parse(pDate); + } + } + catch (ParseException e) { + NumberFormatException nfe = new NumberFormatException("Invalid HTTP date: \"" + pDate + "\""); + nfe.initCause(e); + throw nfe; + } + + if (date == null) { + throw new NumberFormatException("Invalid HTTP date: \"" + pDate + "\""); + } + + return date; + } +} diff --git a/common/common-io/src/test/java/com/twelvemonkeys/net/HTTPUtilTest.java b/common/common-io/src/test/java/com/twelvemonkeys/net/HTTPUtilTest.java new file mode 100644 index 00000000..9bb83f53 --- /dev/null +++ b/common/common-io/src/test/java/com/twelvemonkeys/net/HTTPUtilTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2013, Harald Kuhr + * 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 "TwelveMonkeys" 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 com.twelvemonkeys.net; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * HTTPUtilTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: HTTPUtilTest.java,v 1.0 08.09.13 13:57 haraldk Exp$ + */ +public class HTTPUtilTest { + @Test + public void testParseHTTPDateRFC1123() { + long time = HTTPUtil.parseHTTPDate("Sun, 06 Nov 1994 08:49:37 GMT"); + assertEquals(784111777000l, time); + + time = HTTPUtil.parseHTTPDate("Sunday, 06 Nov 1994 08:49:37 GMT"); + assertEquals(784111777000l, time); + } + + @Test + public void testParseHTTPDateRFC850() { + long time = HTTPUtil.parseHTTPDate("Sunday, 06-Nov-1994 08:49:37 GMT"); + assertEquals(784111777000l, time); + + time = HTTPUtil.parseHTTPDate("Sun, 06-Nov-94 08:49:37 GMT"); + assertEquals(784111777000l, time); + + // NOTE: This test will fail some time, around 2044, + // as the 50 year window will slide... + time = HTTPUtil.parseHTTPDate("Sunday, 06-Nov-94 08:49:37 GMT"); + assertEquals(784111777000l, time); + + time = HTTPUtil.parseHTTPDate("Sun, 06-Nov-94 08:49:37 GMT"); + assertEquals(784111777000l, time); + } + + @Test + public void testParseHTTPDateAsctime() { + long time = HTTPUtil.parseHTTPDate("Sun Nov 6 08:49:37 1994"); + assertEquals(784111777000l, time); + + time = HTTPUtil.parseHTTPDate("Sun Nov 6 08:49:37 94"); + assertEquals(784111777000l, time); + } + + @Test + public void testFormatHTTPDateRFC1123() { + long time = 784111777000l; + assertEquals("Sun, 06 Nov 1994 08:49:37 GMT", HTTPUtil.formatHTTPDate(time)); + } +} diff --git a/common/common-io/src/test/java/com/twelvemonkeys/net/NetUtilTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/net/NetUtilTestCase.java deleted file mode 100755 index f4875444..00000000 --- a/common/common-io/src/test/java/com/twelvemonkeys/net/NetUtilTestCase.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.twelvemonkeys.net; - -import junit.framework.TestCase; - -/** - * NetUtilTestCase - *

- * - * - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/net/NetUtilTestCase.java#1 $ - */ -public class NetUtilTestCase extends TestCase { - public void setUp() throws Exception { - super.setUp(); - } - - public void tearDown() throws Exception { - super.tearDown(); - } - - public void testParseHTTPDateRFC1123() { - long time = NetUtil.parseHTTPDate("Sun, 06 Nov 1994 08:49:37 GMT"); - assertEquals(784111777000l, time); - - time = NetUtil.parseHTTPDate("Sunday, 06 Nov 1994 08:49:37 GMT"); - assertEquals(784111777000l, time); - } - - public void testParseHTTPDateRFC850() { - long time = NetUtil.parseHTTPDate("Sunday, 06-Nov-1994 08:49:37 GMT"); - assertEquals(784111777000l, time); - - time = NetUtil.parseHTTPDate("Sun, 06-Nov-94 08:49:37 GMT"); - assertEquals(784111777000l, time); - - // NOTE: This test will fail some time, around 2044, - // as the 50 year window will slide... - time = NetUtil.parseHTTPDate("Sunday, 06-Nov-94 08:49:37 GMT"); - assertEquals(784111777000l, time); - - time = NetUtil.parseHTTPDate("Sun, 06-Nov-94 08:49:37 GMT"); - assertEquals(784111777000l, time); - } - - public void testParseHTTPDateAsctime() { - long time = NetUtil.parseHTTPDate("Sun Nov 6 08:49:37 1994"); - assertEquals(784111777000l, time); - - time = NetUtil.parseHTTPDate("Sun Nov 6 08:49:37 94"); - assertEquals(784111777000l, time); - } - - public void testFormatHTTPDateRFC1123() { - long time = 784111777000l; - assertEquals("Sun, 06 Nov 1994 08:49:37 GMT", NetUtil.formatHTTPDate(time)); - } -} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java similarity index 97% rename from common/common-io/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java index 6a3f993f..21171d22 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java @@ -1,46 +1,45 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * 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 "TwelveMonkeys" 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 com.twelvemonkeys.net; - -import java.net.*; - -/** - * Interface for filtering Authenticator requests, used by the - * SimpleAuthenticator. - * - * @see SimpleAuthenticator - * @see java.net.Authenticator - * - * @author Harald Kuhr - * @version 1.0 - */ -public interface AuthenticatorFilter { - public boolean accept(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme); - -} +/* + * Copyright (c) 2008, Harald Kuhr + * 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 "TwelveMonkeys" 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 com.twelvemonkeys.net; + +import java.net.*; + +/** + * Interface for filtering Authenticator requests, used by the + * SimpleAuthenticator. + * + * @see SimpleAuthenticator + * @see java.net.Authenticator + * + * @author Harald Kuhr + * @version 1.0 + */ +public interface AuthenticatorFilter { + public boolean accept(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme); +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/BASE64.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/BASE64.java similarity index 97% rename from common/common-io/src/main/java/com/twelvemonkeys/net/BASE64.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/BASE64.java index 0ebbd67d..4d4346f8 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/BASE64.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/BASE64.java @@ -1,144 +1,143 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * 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 "TwelveMonkeys" 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 com.twelvemonkeys.net; - -import com.twelvemonkeys.io.*; -import com.twelvemonkeys.io.enc.Base64Decoder; -import com.twelvemonkeys.io.enc.DecoderStream; - -import java.io.*; - - -/** - * This class does BASE64 encoding (and decoding). - * - * @author unascribed - * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BASE64.java#1 $ - * @deprecated Use {@link com.twelvemonkeys.io.enc.Base64Encoder}/{@link Base64Decoder} instead - */ -class BASE64 { - - /** - * This array maps the characters to their 6 bit values - */ - private final static char[] PEM_ARRAY = { - //0 1 2 3 4 5 6 7 - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 0 - 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 1 - 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 2 - 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // 3 - 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // 4 - 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // 5 - 'w', 'x', 'y', 'z', '0', '1', '2', '3', // 6 - '4', '5', '6', '7', '8', '9', '+', '/' // 7 - }; - - /** - * Encodes the input data using the standard base64 encoding scheme. - * - * @param pData the bytes to encode to base64 - * @return a string with base64 encoded data - */ - public static String encode(byte[] pData) { - int offset = 0; - int len; - StringBuilder buf = new StringBuilder(); - - while ((pData.length - offset) > 0) { - byte a, b, c; - if ((pData.length - offset) > 2) { - len = 3; - } - else { - len = pData.length - offset; - } - - switch (len) { - case 1: - a = pData[offset]; - b = 0; - buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); - buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); - buf.append('='); - buf.append('='); - offset++; - break; - case 2: - a = pData[offset]; - b = pData[offset + 1]; - c = 0; - buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); - buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); - buf.append(PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); - buf.append('='); - offset += offset + 2; // ??? - break; - default: - a = pData[offset]; - b = pData[offset + 1]; - c = pData[offset + 2]; - buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); - buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); - buf.append(PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); - buf.append(PEM_ARRAY[c & 0x3F]); - offset = offset + 3; - break; - } - - } - return buf.toString(); - } - - public static byte[] decode(String pData) throws IOException { - InputStream in = new DecoderStream(new ByteArrayInputStream(pData.getBytes()), new Base64Decoder()); - ByteArrayOutputStream bytes = new FastByteArrayOutputStream(pData.length() * 3); - FileUtil.copy(in, bytes); - - return bytes.toByteArray(); - } - - //private final static sun.misc.BASE64Decoder DECODER = new sun.misc.BASE64Decoder(); - - public static void main(String[] pArgs) throws IOException { - if (pArgs.length == 1) { - System.out.println(encode(pArgs[0].getBytes())); - } - else - if (pArgs.length == 2 && ("-d".equals(pArgs[0]) || "--decode".equals(pArgs[0]))) - { - System.out.println(new String(decode(pArgs[1]))); - } - else { - System.err.println("BASE64 [ -d | --decode ] arg"); - System.err.println("Encodes or decodes a given string"); - System.exit(5); - } - } +/* + * Copyright (c) 2008, Harald Kuhr + * 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 "TwelveMonkeys" 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 com.twelvemonkeys.net; + +import com.twelvemonkeys.io.*; +import com.twelvemonkeys.io.enc.Base64Decoder; +import com.twelvemonkeys.io.enc.DecoderStream; + +import java.io.*; + + +/** + * This class does BASE64 encoding (and decoding). + * + * @author unascribed + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BASE64.java#1 $ + * @deprecated Use {@link com.twelvemonkeys.io.enc.Base64Encoder}/{@link Base64Decoder} instead + */ +class BASE64 { + /** + * This array maps the characters to their 6 bit values + */ + private final static char[] PEM_ARRAY = { + //0 1 2 3 4 5 6 7 + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 0 + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 1 + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 2 + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // 3 + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // 4 + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // 5 + 'w', 'x', 'y', 'z', '0', '1', '2', '3', // 6 + '4', '5', '6', '7', '8', '9', '+', '/' // 7 + }; + + /** + * Encodes the input data using the standard base64 encoding scheme. + * + * @param pData the bytes to encode to base64 + * @return a string with base64 encoded data + */ + public static String encode(byte[] pData) { + int offset = 0; + int len; + StringBuilder buf = new StringBuilder(); + + while ((pData.length - offset) > 0) { + byte a, b, c; + if ((pData.length - offset) > 2) { + len = 3; + } + else { + len = pData.length - offset; + } + + switch (len) { + case 1: + a = pData[offset]; + b = 0; + buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); + buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + buf.append('='); + buf.append('='); + offset++; + break; + case 2: + a = pData[offset]; + b = pData[offset + 1]; + c = 0; + buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); + buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + buf.append(PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + buf.append('='); + offset += offset + 2; // ??? + break; + default: + a = pData[offset]; + b = pData[offset + 1]; + c = pData[offset + 2]; + buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); + buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + buf.append(PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + buf.append(PEM_ARRAY[c & 0x3F]); + offset = offset + 3; + break; + } + + } + return buf.toString(); + } + + public static byte[] decode(String pData) throws IOException { + InputStream in = new DecoderStream(new ByteArrayInputStream(pData.getBytes()), new Base64Decoder()); + ByteArrayOutputStream bytes = new FastByteArrayOutputStream(pData.length() * 3); + FileUtil.copy(in, bytes); + + return bytes.toByteArray(); + } + + //private final static sun.misc.BASE64Decoder DECODER = new sun.misc.BASE64Decoder(); + + public static void main(String[] pArgs) throws IOException { + if (pArgs.length == 1) { + System.out.println(encode(pArgs[0].getBytes())); + } + else + if (pArgs.length == 2 && ("-d".equals(pArgs[0]) || "--decode".equals(pArgs[0]))) + { + System.out.println(new String(decode(pArgs[1]))); + } + else { + System.err.println("BASE64 [ -d | --decode ] arg"); + System.err.println("Encodes or decodes a given string"); + System.exit(5); + } + } } \ No newline at end of file diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java similarity index 97% rename from common/common-io/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java index bd337235..62119c2b 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java @@ -1,1102 +1,1101 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * 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 "TwelveMonkeys" 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 com.twelvemonkeys.net; - -import com.twelvemonkeys.lang.StringUtil; - -import java.io.*; -import java.net.*; -import java.util.*; - -/** - * A URLConnection with support for HTTP-specific features. See - * the spec for details. - * This version also supports read and connect timeouts, making it more useful - * for clients with limitted time. - *

- * Note that the timeouts are created on the socket level, and that - *

- * Note: This class should now work as expected, but it need more testing before - * it can enter production release. - *
- * --.k - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java#1 $ - * @todo Write JUnit TestCase - * @todo ConnectionMananger! - * @see RFC 2616 - */ -public class HttpURLConnection extends java.net.HttpURLConnection { - - /** - * HTTP Status-Code 307: Temporary Redirect - */ - public final static int HTTP_REDIRECT = 307; - private final static int HTTP_DEFAULT_PORT = 80; - private final static String HTTP_HEADER_END = "\r\n\r\n"; - private static final String HEADER_WWW_AUTH = "WWW-Authenticate"; - private final static int BUF_SIZE = 8192; - private int maxRedirects = (System.getProperty("http.maxRedirects") != null) - ? Integer.parseInt(System.getProperty("http.maxRedirects")) - : 20; - protected int timeout = -1; - protected int connectTimeout = -1; - private Socket socket = null; - protected InputStream errorStream = null; - protected InputStream inputStream = null; - protected OutputStream outputStream = null; - private String[] responseHeaders = null; - protected Properties responseHeaderFields = null; - protected Properties requestProperties = new Properties(); - - /** - * Creates a HttpURLConnection. - * - * @param pURL the URL to connect to. - */ - protected HttpURLConnection(URL pURL) { - this(pURL, 0, 0); - } - - /** - * Creates a HttpURLConnection with a given read and connect timeout. - * A timeout value of zero is interpreted as an - * infinite timeout. - * - * @param pURL the URL to connect to. - * @param pTimeout the maximum time the socket will block for read - * and connect operations. - */ - protected HttpURLConnection(URL pURL, int pTimeout) { - this(pURL, pTimeout, pTimeout); - } - - /** - * Creates a HttpURLConnection with a given read and connect timeout. - * A timeout value of zero is interpreted as an - * infinite timeout. - * - * @param pURL the URL to connect to. - * @param pTimeout the maximum time the socket will block for read - * operations. - * @param pConnectTimeout the maximum time the socket will block for - * connection. - */ - protected HttpURLConnection(URL pURL, int pTimeout, int pConnectTimeout) { - super(pURL); - setTimeout(pTimeout); - connectTimeout = pConnectTimeout; - } - - /** - * Sets the general request property. If a property with the key already - * exists, overwrite its value with the new value. - *

- *

NOTE: HTTP requires all request properties which can - * legally have multiple instances with the same key - * to use a comma-seperated list syntax which enables multiple - * properties to be appended into a single property. - * - * @param pKey the keyword by which the request is known - * (e.g., "{@code accept}"). - * @param pValue the value associated with it. - * @see #getRequestProperty(java.lang.String) - */ - public void setRequestProperty(String pKey, String pValue) { - if (connected) { - throw new IllegalAccessError("Already connected"); - } - String oldValue = requestProperties.getProperty(pKey); - - if (oldValue == null) { - requestProperties.setProperty(pKey, pValue); - } - else { - requestProperties.setProperty(pKey, oldValue + ", " + pValue); - } - } - - /** - * Returns the value of the named general request property for this - * connection. - * - * @param pKey the keyword by which the request is known (e.g., "accept"). - * @return the value of the named general request property for this - * connection. - * @see #setRequestProperty(java.lang.String, java.lang.String) - */ - public String getRequestProperty(String pKey) { - if (connected) { - throw new IllegalAccessError("Already connected"); - } - return requestProperties.getProperty(pKey); - } - - /** - * Gets HTTP response status from responses like: - *

-     * HTTP/1.0 200 OK
-     * HTTP/1.0 401 Unauthorized
-     * 
- * Extracts the ints 200 and 401 respectively. - * Returns -1 if none can be discerned - * from the response (i.e., the response is not valid HTTP). - *

- * - * - * @return the HTTP Status-Code - * @throws IOException if an error occurred connecting to the server. - */ - public int getResponseCode() throws IOException { - if (responseCode != -1) { - return responseCode; - } - - // Make sure we've gotten the headers - getInputStream(); - String resp = getHeaderField(0); - - // should have no leading/trailing LWS - // expedite the typical case by assuming it has the - // form "HTTP/1.x 2XX " - int ind; - - try { - ind = resp.indexOf(' '); - while (resp.charAt(ind) == ' ') { - ind++; - } - responseCode = Integer.parseInt(resp.substring(ind, ind + 3)); - responseMessage = resp.substring(ind + 4).trim(); - return responseCode; - } - catch (Exception e) { - return responseCode; - } - } - - /** - * Returns the name of the specified header field. - * - * @param pName the name of a header field. - * @return the value of the named header field, or {@code null} - * if there is no such field in the header. - */ - public String getHeaderField(String pName) { - return responseHeaderFields.getProperty(StringUtil.toLowerCase(pName)); - } - - /** - * Returns the value for the {@code n}th header field. - * It returns {@code null} if there are fewer than - * {@code n} fields. - *

- * This method can be used in conjunction with the - * {@code getHeaderFieldKey} method to iterate through all - * the headers in the message. - * - * @param pIndex an index. - * @return the value of the {@code n}th header field. - * @see java.net.URLConnection#getHeaderFieldKey(int) - */ - public String getHeaderField(int pIndex) { - // TODO: getInputStream() first, to make sure we have header fields - if (pIndex >= responseHeaders.length) { - return null; - } - String field = responseHeaders[pIndex]; - - // pIndex == 0, means the response code etc (i.e. "HTTP/1.1 200 OK"). - if ((pIndex == 0) || (field == null)) { - return field; - } - int idx = field.indexOf(':'); - - return ((idx > 0) - ? field.substring(idx).trim() - : ""); // TODO: "" or null? - } - - /** - * Returns the key for the {@code n}th header field. - * - * @param pIndex an index. - * @return the key for the {@code n}th header field, - * or {@code null} if there are fewer than {@code n} - * fields. - */ - public String getHeaderFieldKey(int pIndex) { - // TODO: getInputStream() first, to make sure we have header fields - if (pIndex >= responseHeaders.length) { - return null; - } - String field = responseHeaders[pIndex]; - - if (StringUtil.isEmpty(field)) { - return null; - } - int idx = field.indexOf(':'); - - return StringUtil.toLowerCase(((idx > 0) - ? field.substring(0, idx) - : field)); - } - - /** - * Sets the read timeout for the undelying socket. - * A timeout of zero is interpreted as an - * infinite timeout. - * - * @param pTimeout the maximum time the socket will block for read - * operations, in milliseconds. - */ - public void setTimeout(int pTimeout) { - if (pTimeout < 0) { // Must be positive - throw new IllegalArgumentException("Timeout must be positive."); - } - timeout = pTimeout; - if (socket != null) { - try { - socket.setSoTimeout(pTimeout); - } - catch (SocketException se) { - // Not much to do about that... - } - } - } - - /** - * Gets the read timeout for the undelying socket. - * - * @return the maximum time the socket will block for read operations, in - * milliseconds. - * The default value is zero, which is interpreted as an - * infinite timeout. - */ - public int getTimeout() { - - try { - return ((socket != null) - ? socket.getSoTimeout() - : timeout); - } - catch (SocketException se) { - return timeout; - } - } - - /** - * Returns an input stream that reads from this open connection. - * - * @return an input stream that reads from this open connection. - * @throws IOException if an I/O error occurs while - * creating the input stream. - */ - public synchronized InputStream getInputStream() throws IOException { - if (!connected) { - connect(); - } - - // Nothing to return - if (responseCode == HTTP_NOT_FOUND) { - throw new FileNotFoundException(url.toString()); - } - int length; - - if (inputStream == null) { - return null; - } - - // "De-chunk" the output stream - else if ("chunked".equalsIgnoreCase(getHeaderField("Transfer-Encoding"))) { - if (!(inputStream instanceof ChunkedInputStream)) { - inputStream = new ChunkedInputStream(inputStream); - } - } - - // Make sure we don't wait forever, if the content-length is known - else if ((length = getHeaderFieldInt("Content-Length", -1)) >= 0) { - if (!(inputStream instanceof FixedLengthInputStream)) { - inputStream = new FixedLengthInputStream(inputStream, length); - } - } - return inputStream; - } - - /** - * Returns an output stream that writes to this connection. - * - * @return an output stream that writes to this connection. - * @throws IOException if an I/O error occurs while - * creating the output stream. - */ - public synchronized OutputStream getOutputStream() throws IOException { - - if (!connected) { - connect(); - } - return outputStream; - } - - /** - * Indicates that other requests to the server - * are unlikely in the near future. Calling disconnect() - * should not imply that this HttpURLConnection - * instance can be reused for other requests. - */ - public void disconnect() { - if (socket != null) { - try { - socket.close(); - } - catch (IOException ioe) { - - // Does not matter, I guess. - } - socket = null; - } - connected = false; - } - - /** - * Internal connect method. - */ - private void connect(final URL pURL, PasswordAuthentication pAuth, String pAuthType, int pRetries) throws IOException { - // Find correct port - final int port = (pURL.getPort() > 0) - ? pURL.getPort() - : HTTP_DEFAULT_PORT; - - // Create socket if we don't have one - if (socket == null) { - //socket = new Socket(pURL.getHost(), port); // Blocks... - socket = createSocket(pURL, port, connectTimeout); - socket.setSoTimeout(timeout); - } - - // Get Socket output stream - OutputStream os = socket.getOutputStream(); - - // Connect using HTTP - writeRequestHeaders(os, pURL, method, requestProperties, usingProxy(), pAuth, pAuthType); - - // Get response input stream - InputStream sis = socket.getInputStream(); - BufferedInputStream is = new BufferedInputStream(sis); - - // Detatch reponse headers from reponse input stream - InputStream header = detatchResponseHeader(is); - - // Parse headers and set response code/message - responseHeaders = parseResponseHeader(header); - responseHeaderFields = parseHeaderFields(responseHeaders); - - //System.err.println("Headers fields:"); - //responseHeaderFields.list(System.err); - // Test HTTP response code, to see if further action is needed - switch (getResponseCode()) { - case HTTP_OK: - // 200 OK - inputStream = is; - errorStream = null; - break; - - /* - case HTTP_PROXY_AUTH: - // 407 Proxy Authentication Required - */ - case HTTP_UNAUTHORIZED: - // 401 Unauthorized - // Set authorization and try again.. Slightly more compatible - responseCode = -1; - - // IS THIS REDIRECTION?? - //if (instanceFollowRedirects) { ??? - String auth = getHeaderField(HEADER_WWW_AUTH); - - // Missing WWW-Authenticate header for 401 response is an error - if (StringUtil.isEmpty(auth)) { - throw new ProtocolException("Missing \"" + HEADER_WWW_AUTH + "\" header for response: 401 " + responseMessage); - } - - // Get real mehtod from WWW-Authenticate header - int SP = auth.indexOf(" "); - String method; - String realm = null; - - if (SP >= 0) { - method = auth.substring(0, SP); - if (auth.length() >= SP + 7) { - realm = auth.substring(SP + 7); // " realm=".lenght() == 7 - } - - // else no realm - } - else { - // Default mehtod is Basic - method = SimpleAuthenticator.BASIC; - } - - // Get PasswordAuthentication - PasswordAuthentication pa = Authenticator.requestPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), port, - pURL.getProtocol(), realm, method); - - // Avoid infinite loop - if (pRetries++ <= 0) { - throw new ProtocolException("Server redirected too many times (" + maxRedirects + ") (Authentication required: " + auth + ")"); // This is what sun.net.www.protocol.http.HttpURLConnection does - } - else if (pa != null) { - connect(pURL, pa, method, pRetries); - } - break; - case HTTP_MOVED_PERM: - // 301 Moved Permanently - case HTTP_MOVED_TEMP: - // 302 Found - case HTTP_SEE_OTHER: - // 303 See Other - /* - case HTTP_USE_PROXY: - // 305 Use Proxy - // How do we handle this? - */ - case HTTP_REDIRECT: - // 307 Temporary Redirect - //System.err.println("Redirecting " + getResponseCode()); - if (instanceFollowRedirects) { - // Redirect - responseCode = -1; // Because of the java.net.URLConnection - - // getResponseCode implementation... - // --- - // I think redirects must be get? - //setRequestMethod("GET"); - // --- - String location = getHeaderField("Location"); - URL newLoc = new URL(pURL, location); - - // Test if we can reuse the Socket - if (!(newLoc.getAuthority().equals(pURL.getAuthority()) && (newLoc.getPort() == pURL.getPort()))) { - socket.close(); // Close the socket, won't need it anymore - socket = null; - } - if (location != null) { - //System.err.println("Redirecting to " + location); - // Avoid infinite loop - if (--pRetries <= 0) { - throw new ProtocolException("Server redirected too many times (5)"); - } - else { - connect(newLoc, pAuth, pAuthType, pRetries); - } - } - break; - } - - // ...else, fall through default (if no Location: header) - default : - // Not 200 OK, or any of the redirect responses - // Probably an error... - errorStream = is; - inputStream = null; - } - - // --- Need rethinking... - // No further questions, let the Socket wait forever (until the server - // closes the connection) - //socket.setSoTimeout(0); - // Probably not... The timeout should only kick if the read BLOCKS. - // Shutdown output, meaning any writes to the outputstream below will - // probably fail... - //socket.shutdownOutput(); - // Not a good idea at all... POSTs need the outputstream to send the - // form-data. - // --- /Need rethinking. - outputStream = os; - } - - private static interface SocketConnector extends Runnable { - - /** - * Method getSocket - * - * @return the socket - * @throws IOException - */ - public Socket getSocket() throws IOException; - } - - /** - * Creates a socket to the given URL and port, with the given connect - * timeout. If the socket waits more than the given timout to connect, - * an ConnectException is thrown. - * - * @param pURL the URL to connect to - * @param pPort the port to connect to - * @param pConnectTimeout the connect timeout - * @return the created Socket. - * @throws ConnectException if the connection is refused or otherwise - * times out. - * @throws UnknownHostException if the IP address of the host could not be - * determined. - * @throws IOException if an I/O error occurs when creating the socket. - * @todo Move this code to a SocetImpl or similar? - * @see Socket#Socket(String,int) - */ - private Socket createSocket(final URL pURL, final int pPort, int pConnectTimeout) throws IOException { - Socket socket; - final Object current = this; - SocketConnector connector; - Thread t = new Thread(connector = new SocketConnector() { - - private IOException mConnectException = null; - private Socket mLocalSocket = null; - - public Socket getSocket() throws IOException { - - if (mConnectException != null) { - throw mConnectException; - } - return mLocalSocket; - } - - // Run method - public void run() { - - try { - mLocalSocket = new Socket(pURL.getHost(), pPort); // Blocks... - } - catch (IOException ioe) { - - // Store this exception for later - mConnectException = ioe; - } - - // Signal that we are done - synchronized (current) { - current.notify(); - } - } - }); - - t.start(); - - // Wait for connect - synchronized (this) { - try { - - /// Only wait if thread is alive! - if (t.isAlive()) { - if (pConnectTimeout > 0) { - wait(pConnectTimeout); - } - else { - wait(); - } - } - } - catch (InterruptedException ie) { - - // Continue excecution on interrupt? Hmmm.. - } - } - - // Throw exception if the socket didn't connect fast enough - if ((socket = connector.getSocket()) == null) { - throw new ConnectException("Socket connect timed out!"); - } - return socket; - } - - /** - * Opens a communications link to the resource referenced by this - * URL, if such a connection has not already been established. - *

- * If the {@code connect} method is called when the connection - * has already been opened (indicated by the {@code connected} - * field having the value {@code true}), the call is ignored. - *

- * URLConnection objects go through two phases: first they are - * created, then they are connected. After being created, and - * before being connected, various options can be specified - * (e.g., doInput and UseCaches). After connecting, it is an - * error to try to set them. Operations that depend on being - * connected, like getContentLength, will implicitly perform the - * connection, if necessary. - * - * @throws IOException if an I/O error occurs while opening the - * connection. - * @see java.net.URLConnection#connected - * @see RFC 2616 - */ - public void connect() throws IOException { - if (connected) { - return; // Ignore - } - connected = true; - connect(url, null, null, maxRedirects); - } - - /** - * TODO: Proxy support is still missing. - * - * @return this method returns false, as proxy suport is not implemented. - */ - public boolean usingProxy() { - return false; - } - - /** - * Writes the HTTP request headers, for HTTP GET method. - * - * @see RFC 2616 - */ - private static void writeRequestHeaders(OutputStream pOut, URL pURL, String pMethod, Properties pProps, boolean pUsingProxy, - PasswordAuthentication pAuth, String pAuthType) { - PrintWriter out = new PrintWriter(pOut, true); // autoFlush - - if (!pUsingProxy) { - out.println(pMethod + " " + (!StringUtil.isEmpty(pURL.getPath()) - ? pURL.getPath() - : "/") + ((pURL.getQuery() != null) - ? "?" + pURL.getQuery() - : "") + " HTTP/1.1"); // HTTP/1.1 - - // out.println("Connection: close"); // No persistent connections yet - - /* - System.err.println(pMethod + " " - + (!StringUtil.isEmpty(pURL.getPath()) ? pURL.getPath() : "/") - + (pURL.getQuery() != null ? "?" + pURL.getQuery() : "") - + " HTTP/1.1"); // HTTP/1.1 - */ - - // Authority (Host: HTTP/1.1 field, but seems to work for HTTP/1.0) - out.println("Host: " + pURL.getHost() + ((pURL.getPort() != -1) - ? ":" + pURL.getPort() - : "")); - - /* - System.err.println("Host: " + pURL.getHost() - + (pURL.getPort() != -1 ? ":" + pURL.getPort() : "")); - */ - } - else { - - ////-- PROXY (absolute) VERSION - out.println(pMethod + " " + pURL.getProtocol() + "://" + pURL.getHost() + ((pURL.getPort() != -1) - ? ":" + pURL.getPort() - : "") + pURL.getPath() + ((pURL.getQuery() != null) - ? "?" + pURL.getQuery() - : "") + " HTTP/1.1"); - } - - // Check if we have authentication - if (pAuth != null) { - - // If found, set Authorization header - byte[] userPass = (pAuth.getUserName() + ":" + new String(pAuth.getPassword())).getBytes(); - - // "Authorization" ":" credentials - out.println("Authorization: " + pAuthType + " " + BASE64.encode(userPass)); - - /* - System.err.println("Authorization: " + pAuthType + " " - + BASE64.encode(userPass)); - */ - } - - // Iterate over properties - - for (Map.Entry property : pProps.entrySet()) { - out.println(property.getKey() + ": " + property.getValue()); - - //System.err.println(property.getKey() + ": " + property.getValue()); - } - out.println(); // Empty line, marks end of request-header - } - - /** - * Finds the end of the HTTP response header in an array of bytes. - * - * @todo This one's a little dirty... - */ - private static int findEndOfHeader(byte[] pBytes, int pEnd) { - byte[] header = HTTP_HEADER_END.getBytes(); - - // Normal condition, check all bytes - for (int i = 0; i < pEnd - 4; i++) { // Need 4 bytes to match - if ((pBytes[i] == header[0]) && (pBytes[i + 1] == header[1]) && (pBytes[i + 2] == header[2]) && (pBytes[i + 3] == header[3])) { - - //System.err.println("FOUND END OF HEADER!"); - return i + 4; - } - } - - // Check last 3 bytes, to check if we have a partial match - if ((pEnd - 1 >= 0) && (pBytes[pEnd - 1] == header[0])) { - - //System.err.println("FOUND LAST BYTE"); - return -2; // LAST BYTE - } - else if ((pEnd - 2 >= 0) && (pBytes[pEnd - 2] == header[0]) && (pBytes[pEnd - 1] == header[1])) { - - //System.err.println("FOUND LAST TWO BYTES"); - return -3; // LAST TWO BYTES - } - else if ((pEnd - 3 >= 0) && (pBytes[pEnd - 3] == header[0]) && (pBytes[pEnd - 2] == header[1]) && (pBytes[pEnd - 1] == header[2])) { - - //System.err.println("FOUND LAST THREE BYTES"); - return -4; // LAST THREE BYTES - } - return -1; // NO BYTES MATCH - } - - /** - * Reads the header part of the response, and copies it to a different - * InputStream. - */ - private static InputStream detatchResponseHeader(BufferedInputStream pIS) throws IOException { - // Store header in byte array - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - pIS.mark(BUF_SIZE); - byte[] buffer = new byte[BUF_SIZE]; - int length; - int headerEnd; - - // Read from iput, store in bytes - while ((length = pIS.read(buffer)) != -1) { - - // End of header? - headerEnd = findEndOfHeader(buffer, length); - if (headerEnd >= 0) { - - // Write rest - bytes.write(buffer, 0, headerEnd); - - // Go back to last mark - pIS.reset(); - - // Position stream to right after header, and exit loop - pIS.skip(headerEnd); - break; - } - else if (headerEnd < -1) { - - // Write partial (except matching header bytes) - bytes.write(buffer, 0, length - 4); - - // Go back to last mark - pIS.reset(); - - // Position stream to right before potential header end - pIS.skip(length - 4); - } - else { - - // Write all - bytes.write(buffer, 0, length); - } - - // Can't read more than BUF_SIZE ahead anyway - pIS.mark(BUF_SIZE); - } - return new ByteArrayInputStream(bytes.toByteArray()); - } - - /** - * Pareses the response header fields. - */ - private static Properties parseHeaderFields(String[] pHeaders) { - Properties headers = new Properties(); - - // Get header information - int split; - String field; - String value; - - for (String header : pHeaders) { - //System.err.println(pHeaders[i]); - if ((split = header.indexOf(":")) > 0) { - - // Read & parse..? - field = header.substring(0, split); - value = header.substring(split + 1); - - //System.err.println(field + ": " + value.trim()); - headers.setProperty(StringUtil.toLowerCase(field), value.trim()); - } - } - return headers; - } - - /** - * Parses the response headers. - */ - private static String[] parseResponseHeader(InputStream pIS) throws IOException { - List headers = new ArrayList(); - - // Wrap Stream in Reader - BufferedReader in = new BufferedReader(new InputStreamReader(pIS)); - - // Get response status - String header; - - while ((header = in.readLine()) != null) { - //System.err.println(header); - headers.add(header); - } - return headers.toArray(new String[headers.size()]); - } - - /** - * A FilterInputStream that wraps HTTP streams, with given content-length. - */ - protected static class FixedLengthInputStream extends FilterInputStream { - - private int mBytesLeft = 0; - - protected FixedLengthInputStream(InputStream pIS, int pLength) { - super(pIS); - mBytesLeft = pLength; - } - - public int available() throws IOException { - int available = in.available(); - - return ((available < mBytesLeft) - ? available - : mBytesLeft); - } - - public int read() throws IOException { - if (mBytesLeft-- > 0) { - return in.read(); - } - return -1; - } - - public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { - int read; - - if (mBytesLeft <= 0) { - return -1; // EOF - } - else if (mBytesLeft < pLength) { - - // Read all available - read = in.read(pBytes, pOffset, mBytesLeft); - - //System.err.println("Reading partial: " + read); - mBytesLeft -= read; - return read; - } - - // Just read - read = in.read(pBytes, pOffset, pLength); - - //System.err.println("Reading all avail: " + read); - mBytesLeft -= read; - return read; - } - } - - /** - * A FilterInputStream that wraps HTTP 1.1 "chunked" transfer mode. - */ - protected static class ChunkedInputStream extends FilterInputStream { - - private int mAvailableInCurrentChunk = 0; - - /** - * Creates an input streams that removes the "chunk-headers" and - * makes it look like any other input stream. - */ - protected ChunkedInputStream(InputStream pIS) { - - super(pIS); - if (pIS == null) { - throw new IllegalArgumentException("InputStream may not be null!"); - } - } - - /** - * Returns the number of bytes that can be read from this input stream - * without blocking. - *

- * This version returns whatever is less of in.available() and the - * length of the current chunk. - * - * @return the number of bytes that can be read from the input stream - * without blocking. - * @throws IOException if an I/O error occurs. - * @see #in - */ - public int available() throws IOException { - - if (mAvailableInCurrentChunk == 0) { - mAvailableInCurrentChunk = parseChunkSize(); - } - int realAvail = in.available(); - - return (mAvailableInCurrentChunk < realAvail) - ? mAvailableInCurrentChunk - : realAvail; - } - - /** - * Reads up to len bytes of data from this input stream into an array - * of bytes. This method blocks until some input is available. - *

- * This version will read up to len bytes of data, or as much as is - * available in the current chunk. If there is no more data in the - * curernt chunk, the method will read the size of the next chunk, and - * read from that, until the last chunk is read (a chunk with a size of - * 0). - * - * @param pBytes the buffer into which the data is read. - * @param pOffset the start offset of the data. - * @param pLength the maximum number of bytes read. - * @return the total number of bytes read into the buffer, or -1 if - * there is no more data because the end of the stream has been - * reached. - * @throws IOException if an I/O error occurs. - * @see #in - */ - public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { - - //System.err.println("Avail: " + mAvailableInCurrentChunk - // + " length: " + pLength); - int read; - - if (mAvailableInCurrentChunk == -1) { - return -1; // EOF - } - if (mAvailableInCurrentChunk == 0) { - - //System.err.println("Nothing to read, parsing size!"); - // If nothing is read so far, read chunk header - mAvailableInCurrentChunk = parseChunkSize(); - return read(pBytes, pOffset, pLength); - } - else if (mAvailableInCurrentChunk < pLength) { - - // Read all available - read = in.read(pBytes, pOffset, mAvailableInCurrentChunk); - - //System.err.println("Reading partial: " + read); - mAvailableInCurrentChunk -= read; - return read; - } - - // Just read - read = in.read(pBytes, pOffset, pLength); - - //System.err.println("Reading all avail: " + read); - mAvailableInCurrentChunk -= read; - return read; - } - - /** - * Reads the next byte of data from this input stream. The value byte - * is returned as an int in the range 0 to 255. If no byte is available - * because the end of the stream has been reached, the value -1 is - * returned. This method blocks until input data is available, the end - * of the stream is detected, or an exception is thrown. - *

- * This version reads one byte of data from the current chunk as long - * as there is more data in the chunk. If there is no more data in the - * curernt chunk, the method will read the size of the next chunk, and - * read from that, until the last chunk is read (a chunk with a size of - * 0). - * - * @return the next byte of data, or -1 if the end of the stream is - * reached. - * @see #in - */ - public int read() throws IOException { - - // We have no data, parse chunk header - if (mAvailableInCurrentChunk == -1) { - return -1; - } - else if (mAvailableInCurrentChunk == 0) { - - // Next chunk! - mAvailableInCurrentChunk = parseChunkSize(); - return read(); - } - mAvailableInCurrentChunk--; - return in.read(); - } - - /** - * Reads the chunk size from the chunk header - * {@code chunk-size [SP chunk-extension] CRLF}. - * The chunk-extension is simply discarded. - * - * @return the length of the current chunk, or -1 if the current chunk - * is the last-chunk (a chunk with the size of 0). - */ - protected int parseChunkSize() throws IOException { - - StringBuilder buf = new StringBuilder(); - int b; - - // read chunk-size, chunk-extension (if any) and CRLF - while ((b = in.read()) > 0) { - if ((b == '\r') && (in.read() == '\n')) { // Should be no CR or LF - break; // except for this one... - } - buf.append((char) b); - } - String line = buf.toString(); - - // Happens, as we don't read CRLF off the end of the chunk data... - if (line.length() == 0) { - return 0; - } - - // Discard any chunk-extensions, and read size (HEX). - int spIdx = line.indexOf(' '); - int size = Integer.parseInt(((spIdx >= 0) - ? line.substring(0, spIdx) - : line), 16); - - // This is the last chunk (=EOF) - if (size == 0) { - return -1; - } - return size; - } - } -} +/* + * Copyright (c) 2008, Harald Kuhr + * 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 "TwelveMonkeys" 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 com.twelvemonkeys.net; + +import com.twelvemonkeys.lang.StringUtil; + +import java.io.*; +import java.net.*; +import java.util.*; + +/** + * A URLConnection with support for HTTP-specific features. See + * the spec for details. + * This version also supports read and connect timeouts, making it more useful + * for clients with limitted time. + *

+ * Note that the timeouts are created on the socket level, and that + *

+ * Note: This class should now work as expected, but it needs more testing before + * it can enter production release. + *
+ * --.k + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java#1 $ + * @todo Write JUnit TestCase + * @todo ConnectionMananger! + * @see RFC 2616 + */ +public class HttpURLConnection extends java.net.HttpURLConnection { + /** + * HTTP Status-Code 307: Temporary Redirect + */ + public final static int HTTP_REDIRECT = 307; + private final static int HTTP_DEFAULT_PORT = 80; + private final static String HTTP_HEADER_END = "\r\n\r\n"; + private static final String HEADER_WWW_AUTH = "WWW-Authenticate"; + private final static int BUF_SIZE = 8192; + private int maxRedirects = (System.getProperty("http.maxRedirects") != null) + ? Integer.parseInt(System.getProperty("http.maxRedirects")) + : 20; + protected int timeout = -1; + protected int connectTimeout = -1; + private Socket socket = null; + protected InputStream errorStream = null; + protected InputStream inputStream = null; + protected OutputStream outputStream = null; + private String[] responseHeaders = null; + protected Properties responseHeaderFields = null; + protected Properties requestProperties = new Properties(); + + /** + * Creates a HttpURLConnection. + * + * @param pURL the URL to connect to. + */ + protected HttpURLConnection(URL pURL) { + this(pURL, 0, 0); + } + + /** + * Creates a HttpURLConnection with a given read and connect timeout. + * A timeout value of zero is interpreted as an + * infinite timeout. + * + * @param pURL the URL to connect to. + * @param pTimeout the maximum time the socket will block for read + * and connect operations. + */ + protected HttpURLConnection(URL pURL, int pTimeout) { + this(pURL, pTimeout, pTimeout); + } + + /** + * Creates a HttpURLConnection with a given read and connect timeout. + * A timeout value of zero is interpreted as an + * infinite timeout. + * + * @param pURL the URL to connect to. + * @param pTimeout the maximum time the socket will block for read + * operations. + * @param pConnectTimeout the maximum time the socket will block for + * connection. + */ + protected HttpURLConnection(URL pURL, int pTimeout, int pConnectTimeout) { + super(pURL); + setTimeout(pTimeout); + connectTimeout = pConnectTimeout; + } + + /** + * Sets the general request property. If a property with the key already + * exists, overwrite its value with the new value. + *

+ *

NOTE: HTTP requires all request properties which can + * legally have multiple instances with the same key + * to use a comma-seperated list syntax which enables multiple + * properties to be appended into a single property. + * + * @param pKey the keyword by which the request is known + * (e.g., "{@code accept}"). + * @param pValue the value associated with it. + * @see #getRequestProperty(java.lang.String) + */ + public void setRequestProperty(String pKey, String pValue) { + if (connected) { + throw new IllegalAccessError("Already connected"); + } + String oldValue = requestProperties.getProperty(pKey); + + if (oldValue == null) { + requestProperties.setProperty(pKey, pValue); + } + else { + requestProperties.setProperty(pKey, oldValue + ", " + pValue); + } + } + + /** + * Returns the value of the named general request property for this + * connection. + * + * @param pKey the keyword by which the request is known (e.g., "accept"). + * @return the value of the named general request property for this + * connection. + * @see #setRequestProperty(java.lang.String, java.lang.String) + */ + public String getRequestProperty(String pKey) { + if (connected) { + throw new IllegalAccessError("Already connected"); + } + return requestProperties.getProperty(pKey); + } + + /** + * Gets HTTP response status from responses like: + *

+     * HTTP/1.0 200 OK
+     * HTTP/1.0 401 Unauthorized
+     * 
+ * Extracts the ints 200 and 401 respectively. + * Returns -1 if none can be discerned + * from the response (i.e., the response is not valid HTTP). + *

+ * + * + * @return the HTTP Status-Code + * @throws IOException if an error occurred connecting to the server. + */ + public int getResponseCode() throws IOException { + if (responseCode != -1) { + return responseCode; + } + + // Make sure we've gotten the headers + getInputStream(); + String resp = getHeaderField(0); + + // should have no leading/trailing LWS + // expedite the typical case by assuming it has the + // form "HTTP/1.x 2XX " + int ind; + + try { + ind = resp.indexOf(' '); + while (resp.charAt(ind) == ' ') { + ind++; + } + responseCode = Integer.parseInt(resp.substring(ind, ind + 3)); + responseMessage = resp.substring(ind + 4).trim(); + return responseCode; + } + catch (Exception e) { + return responseCode; + } + } + + /** + * Returns the name of the specified header field. + * + * @param pName the name of a header field. + * @return the value of the named header field, or {@code null} + * if there is no such field in the header. + */ + public String getHeaderField(String pName) { + return responseHeaderFields.getProperty(StringUtil.toLowerCase(pName)); + } + + /** + * Returns the value for the {@code n}th header field. + * It returns {@code null} if there are fewer than + * {@code n} fields. + *

+ * This method can be used in conjunction with the + * {@code getHeaderFieldKey} method to iterate through all + * the headers in the message. + * + * @param pIndex an index. + * @return the value of the {@code n}th header field. + * @see java.net.URLConnection#getHeaderFieldKey(int) + */ + public String getHeaderField(int pIndex) { + // TODO: getInputStream() first, to make sure we have header fields + if (pIndex >= responseHeaders.length) { + return null; + } + String field = responseHeaders[pIndex]; + + // pIndex == 0, means the response code etc (i.e. "HTTP/1.1 200 OK"). + if ((pIndex == 0) || (field == null)) { + return field; + } + int idx = field.indexOf(':'); + + return ((idx > 0) + ? field.substring(idx).trim() + : ""); // TODO: "" or null? + } + + /** + * Returns the key for the {@code n}th header field. + * + * @param pIndex an index. + * @return the key for the {@code n}th header field, + * or {@code null} if there are fewer than {@code n} + * fields. + */ + public String getHeaderFieldKey(int pIndex) { + // TODO: getInputStream() first, to make sure we have header fields + if (pIndex >= responseHeaders.length) { + return null; + } + String field = responseHeaders[pIndex]; + + if (StringUtil.isEmpty(field)) { + return null; + } + int idx = field.indexOf(':'); + + return StringUtil.toLowerCase(((idx > 0) + ? field.substring(0, idx) + : field)); + } + + /** + * Sets the read timeout for the undelying socket. + * A timeout of zero is interpreted as an + * infinite timeout. + * + * @param pTimeout the maximum time the socket will block for read + * operations, in milliseconds. + */ + public void setTimeout(int pTimeout) { + if (pTimeout < 0) { // Must be positive + throw new IllegalArgumentException("Timeout must be positive."); + } + timeout = pTimeout; + if (socket != null) { + try { + socket.setSoTimeout(pTimeout); + } + catch (SocketException se) { + // Not much to do about that... + } + } + } + + /** + * Gets the read timeout for the undelying socket. + * + * @return the maximum time the socket will block for read operations, in + * milliseconds. + * The default value is zero, which is interpreted as an + * infinite timeout. + */ + public int getTimeout() { + + try { + return ((socket != null) + ? socket.getSoTimeout() + : timeout); + } + catch (SocketException se) { + return timeout; + } + } + + /** + * Returns an input stream that reads from this open connection. + * + * @return an input stream that reads from this open connection. + * @throws IOException if an I/O error occurs while + * creating the input stream. + */ + public synchronized InputStream getInputStream() throws IOException { + if (!connected) { + connect(); + } + + // Nothing to return + if (responseCode == HTTP_NOT_FOUND) { + throw new FileNotFoundException(url.toString()); + } + int length; + + if (inputStream == null) { + return null; + } + + // "De-chunk" the output stream + else if ("chunked".equalsIgnoreCase(getHeaderField("Transfer-Encoding"))) { + if (!(inputStream instanceof ChunkedInputStream)) { + inputStream = new ChunkedInputStream(inputStream); + } + } + + // Make sure we don't wait forever, if the content-length is known + else if ((length = getHeaderFieldInt("Content-Length", -1)) >= 0) { + if (!(inputStream instanceof FixedLengthInputStream)) { + inputStream = new FixedLengthInputStream(inputStream, length); + } + } + return inputStream; + } + + /** + * Returns an output stream that writes to this connection. + * + * @return an output stream that writes to this connection. + * @throws IOException if an I/O error occurs while + * creating the output stream. + */ + public synchronized OutputStream getOutputStream() throws IOException { + + if (!connected) { + connect(); + } + return outputStream; + } + + /** + * Indicates that other requests to the server + * are unlikely in the near future. Calling disconnect() + * should not imply that this HttpURLConnection + * instance can be reused for other requests. + */ + public void disconnect() { + if (socket != null) { + try { + socket.close(); + } + catch (IOException ioe) { + + // Does not matter, I guess. + } + socket = null; + } + connected = false; + } + + /** + * Internal connect method. + */ + private void connect(final URL pURL, PasswordAuthentication pAuth, String pAuthType, int pRetries) throws IOException { + // Find correct port + final int port = (pURL.getPort() > 0) + ? pURL.getPort() + : HTTP_DEFAULT_PORT; + + // Create socket if we don't have one + if (socket == null) { + //socket = new Socket(pURL.getHost(), port); // Blocks... + socket = createSocket(pURL, port, connectTimeout); + socket.setSoTimeout(timeout); + } + + // Get Socket output stream + OutputStream os = socket.getOutputStream(); + + // Connect using HTTP + writeRequestHeaders(os, pURL, method, requestProperties, usingProxy(), pAuth, pAuthType); + + // Get response input stream + InputStream sis = socket.getInputStream(); + BufferedInputStream is = new BufferedInputStream(sis); + + // Detatch reponse headers from reponse input stream + InputStream header = detatchResponseHeader(is); + + // Parse headers and set response code/message + responseHeaders = parseResponseHeader(header); + responseHeaderFields = parseHeaderFields(responseHeaders); + + //System.err.println("Headers fields:"); + //responseHeaderFields.list(System.err); + // Test HTTP response code, to see if further action is needed + switch (getResponseCode()) { + case HTTP_OK: + // 200 OK + inputStream = is; + errorStream = null; + break; + + /* + case HTTP_PROXY_AUTH: + // 407 Proxy Authentication Required + */ + case HTTP_UNAUTHORIZED: + // 401 Unauthorized + // Set authorization and try again.. Slightly more compatible + responseCode = -1; + + // IS THIS REDIRECTION?? + //if (instanceFollowRedirects) { ??? + String auth = getHeaderField(HEADER_WWW_AUTH); + + // Missing WWW-Authenticate header for 401 response is an error + if (StringUtil.isEmpty(auth)) { + throw new ProtocolException("Missing \"" + HEADER_WWW_AUTH + "\" header for response: 401 " + responseMessage); + } + + // Get real mehtod from WWW-Authenticate header + int SP = auth.indexOf(" "); + String method; + String realm = null; + + if (SP >= 0) { + method = auth.substring(0, SP); + if (auth.length() >= SP + 7) { + realm = auth.substring(SP + 7); // " realm=".lenght() == 7 + } + + // else no realm + } + else { + // Default mehtod is Basic + method = SimpleAuthenticator.BASIC; + } + + // Get PasswordAuthentication + PasswordAuthentication pa = Authenticator.requestPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), port, + pURL.getProtocol(), realm, method); + + // Avoid infinite loop + if (pRetries++ <= 0) { + throw new ProtocolException("Server redirected too many times (" + maxRedirects + ") (Authentication required: " + auth + ")"); // This is what sun.net.www.protocol.http.HttpURLConnection does + } + else if (pa != null) { + connect(pURL, pa, method, pRetries); + } + break; + case HTTP_MOVED_PERM: + // 301 Moved Permanently + case HTTP_MOVED_TEMP: + // 302 Found + case HTTP_SEE_OTHER: + // 303 See Other + /* + case HTTP_USE_PROXY: + // 305 Use Proxy + // How do we handle this? + */ + case HTTP_REDIRECT: + // 307 Temporary Redirect + //System.err.println("Redirecting " + getResponseCode()); + if (instanceFollowRedirects) { + // Redirect + responseCode = -1; // Because of the java.net.URLConnection + + // getResponseCode implementation... + // --- + // I think redirects must be get? + //setRequestMethod("GET"); + // --- + String location = getHeaderField("Location"); + URL newLoc = new URL(pURL, location); + + // Test if we can reuse the Socket + if (!(newLoc.getAuthority().equals(pURL.getAuthority()) && (newLoc.getPort() == pURL.getPort()))) { + socket.close(); // Close the socket, won't need it anymore + socket = null; + } + if (location != null) { + //System.err.println("Redirecting to " + location); + // Avoid infinite loop + if (--pRetries <= 0) { + throw new ProtocolException("Server redirected too many times (5)"); + } + else { + connect(newLoc, pAuth, pAuthType, pRetries); + } + } + break; + } + + // ...else, fall through default (if no Location: header) + default : + // Not 200 OK, or any of the redirect responses + // Probably an error... + errorStream = is; + inputStream = null; + } + + // --- Need rethinking... + // No further questions, let the Socket wait forever (until the server + // closes the connection) + //socket.setSoTimeout(0); + // Probably not... The timeout should only kick if the read BLOCKS. + // Shutdown output, meaning any writes to the outputstream below will + // probably fail... + //socket.shutdownOutput(); + // Not a good idea at all... POSTs need the outputstream to send the + // form-data. + // --- /Need rethinking. + outputStream = os; + } + + private static interface SocketConnector extends Runnable { + + /** + * Method getSocket + * + * @return the socket + * @throws IOException + */ + public Socket getSocket() throws IOException; + } + + /** + * Creates a socket to the given URL and port, with the given connect + * timeout. If the socket waits more than the given timout to connect, + * an ConnectException is thrown. + * + * @param pURL the URL to connect to + * @param pPort the port to connect to + * @param pConnectTimeout the connect timeout + * @return the created Socket. + * @throws ConnectException if the connection is refused or otherwise + * times out. + * @throws UnknownHostException if the IP address of the host could not be + * determined. + * @throws IOException if an I/O error occurs when creating the socket. + * @todo Move this code to a SocetImpl or similar? + * @see Socket#Socket(String,int) + */ + private Socket createSocket(final URL pURL, final int pPort, int pConnectTimeout) throws IOException { + Socket socket; + final Object current = this; + SocketConnector connector; + Thread t = new Thread(connector = new SocketConnector() { + + private IOException mConnectException = null; + private Socket mLocalSocket = null; + + public Socket getSocket() throws IOException { + + if (mConnectException != null) { + throw mConnectException; + } + return mLocalSocket; + } + + // Run method + public void run() { + + try { + mLocalSocket = new Socket(pURL.getHost(), pPort); // Blocks... + } + catch (IOException ioe) { + + // Store this exception for later + mConnectException = ioe; + } + + // Signal that we are done + synchronized (current) { + current.notify(); + } + } + }); + + t.start(); + + // Wait for connect + synchronized (this) { + try { + + /// Only wait if thread is alive! + if (t.isAlive()) { + if (pConnectTimeout > 0) { + wait(pConnectTimeout); + } + else { + wait(); + } + } + } + catch (InterruptedException ie) { + + // Continue excecution on interrupt? Hmmm.. + } + } + + // Throw exception if the socket didn't connect fast enough + if ((socket = connector.getSocket()) == null) { + throw new ConnectException("Socket connect timed out!"); + } + return socket; + } + + /** + * Opens a communications link to the resource referenced by this + * URL, if such a connection has not already been established. + *

+ * If the {@code connect} method is called when the connection + * has already been opened (indicated by the {@code connected} + * field having the value {@code true}), the call is ignored. + *

+ * URLConnection objects go through two phases: first they are + * created, then they are connected. After being created, and + * before being connected, various options can be specified + * (e.g., doInput and UseCaches). After connecting, it is an + * error to try to set them. Operations that depend on being + * connected, like getContentLength, will implicitly perform the + * connection, if necessary. + * + * @throws IOException if an I/O error occurs while opening the + * connection. + * @see java.net.URLConnection#connected + * @see RFC 2616 + */ + public void connect() throws IOException { + if (connected) { + return; // Ignore + } + connected = true; + connect(url, null, null, maxRedirects); + } + + /** + * TODO: Proxy support is still missing. + * + * @return this method returns false, as proxy suport is not implemented. + */ + public boolean usingProxy() { + return false; + } + + /** + * Writes the HTTP request headers, for HTTP GET method. + * + * @see RFC 2616 + */ + private static void writeRequestHeaders(OutputStream pOut, URL pURL, String pMethod, Properties pProps, boolean pUsingProxy, + PasswordAuthentication pAuth, String pAuthType) { + PrintWriter out = new PrintWriter(pOut, true); // autoFlush + + if (!pUsingProxy) { + out.println(pMethod + " " + (!StringUtil.isEmpty(pURL.getPath()) + ? pURL.getPath() + : "/") + ((pURL.getQuery() != null) + ? "?" + pURL.getQuery() + : "") + " HTTP/1.1"); // HTTP/1.1 + + // out.println("Connection: close"); // No persistent connections yet + + /* + System.err.println(pMethod + " " + + (!StringUtil.isEmpty(pURL.getPath()) ? pURL.getPath() : "/") + + (pURL.getQuery() != null ? "?" + pURL.getQuery() : "") + + " HTTP/1.1"); // HTTP/1.1 + */ + + // Authority (Host: HTTP/1.1 field, but seems to work for HTTP/1.0) + out.println("Host: " + pURL.getHost() + ((pURL.getPort() != -1) + ? ":" + pURL.getPort() + : "")); + + /* + System.err.println("Host: " + pURL.getHost() + + (pURL.getPort() != -1 ? ":" + pURL.getPort() : "")); + */ + } + else { + + ////-- PROXY (absolute) VERSION + out.println(pMethod + " " + pURL.getProtocol() + "://" + pURL.getHost() + ((pURL.getPort() != -1) + ? ":" + pURL.getPort() + : "") + pURL.getPath() + ((pURL.getQuery() != null) + ? "?" + pURL.getQuery() + : "") + " HTTP/1.1"); + } + + // Check if we have authentication + if (pAuth != null) { + + // If found, set Authorization header + byte[] userPass = (pAuth.getUserName() + ":" + new String(pAuth.getPassword())).getBytes(); + + // "Authorization" ":" credentials + out.println("Authorization: " + pAuthType + " " + BASE64.encode(userPass)); + + /* + System.err.println("Authorization: " + pAuthType + " " + + BASE64.encode(userPass)); + */ + } + + // Iterate over properties + + for (Map.Entry property : pProps.entrySet()) { + out.println(property.getKey() + ": " + property.getValue()); + + //System.err.println(property.getKey() + ": " + property.getValue()); + } + out.println(); // Empty line, marks end of request-header + } + + /** + * Finds the end of the HTTP response header in an array of bytes. + * + * @todo This one's a little dirty... + */ + private static int findEndOfHeader(byte[] pBytes, int pEnd) { + byte[] header = HTTP_HEADER_END.getBytes(); + + // Normal condition, check all bytes + for (int i = 0; i < pEnd - 4; i++) { // Need 4 bytes to match + if ((pBytes[i] == header[0]) && (pBytes[i + 1] == header[1]) && (pBytes[i + 2] == header[2]) && (pBytes[i + 3] == header[3])) { + + //System.err.println("FOUND END OF HEADER!"); + return i + 4; + } + } + + // Check last 3 bytes, to check if we have a partial match + if ((pEnd - 1 >= 0) && (pBytes[pEnd - 1] == header[0])) { + + //System.err.println("FOUND LAST BYTE"); + return -2; // LAST BYTE + } + else if ((pEnd - 2 >= 0) && (pBytes[pEnd - 2] == header[0]) && (pBytes[pEnd - 1] == header[1])) { + + //System.err.println("FOUND LAST TWO BYTES"); + return -3; // LAST TWO BYTES + } + else if ((pEnd - 3 >= 0) && (pBytes[pEnd - 3] == header[0]) && (pBytes[pEnd - 2] == header[1]) && (pBytes[pEnd - 1] == header[2])) { + + //System.err.println("FOUND LAST THREE BYTES"); + return -4; // LAST THREE BYTES + } + return -1; // NO BYTES MATCH + } + + /** + * Reads the header part of the response, and copies it to a different + * InputStream. + */ + private static InputStream detatchResponseHeader(BufferedInputStream pIS) throws IOException { + // Store header in byte array + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + pIS.mark(BUF_SIZE); + byte[] buffer = new byte[BUF_SIZE]; + int length; + int headerEnd; + + // Read from iput, store in bytes + while ((length = pIS.read(buffer)) != -1) { + + // End of header? + headerEnd = findEndOfHeader(buffer, length); + if (headerEnd >= 0) { + + // Write rest + bytes.write(buffer, 0, headerEnd); + + // Go back to last mark + pIS.reset(); + + // Position stream to right after header, and exit loop + pIS.skip(headerEnd); + break; + } + else if (headerEnd < -1) { + + // Write partial (except matching header bytes) + bytes.write(buffer, 0, length - 4); + + // Go back to last mark + pIS.reset(); + + // Position stream to right before potential header end + pIS.skip(length - 4); + } + else { + + // Write all + bytes.write(buffer, 0, length); + } + + // Can't read more than BUF_SIZE ahead anyway + pIS.mark(BUF_SIZE); + } + return new ByteArrayInputStream(bytes.toByteArray()); + } + + /** + * Pareses the response header fields. + */ + private static Properties parseHeaderFields(String[] pHeaders) { + Properties headers = new Properties(); + + // Get header information + int split; + String field; + String value; + + for (String header : pHeaders) { + //System.err.println(pHeaders[i]); + if ((split = header.indexOf(":")) > 0) { + + // Read & parse..? + field = header.substring(0, split); + value = header.substring(split + 1); + + //System.err.println(field + ": " + value.trim()); + headers.setProperty(StringUtil.toLowerCase(field), value.trim()); + } + } + return headers; + } + + /** + * Parses the response headers. + */ + private static String[] parseResponseHeader(InputStream pIS) throws IOException { + List headers = new ArrayList(); + + // Wrap Stream in Reader + BufferedReader in = new BufferedReader(new InputStreamReader(pIS)); + + // Get response status + String header; + + while ((header = in.readLine()) != null) { + //System.err.println(header); + headers.add(header); + } + return headers.toArray(new String[headers.size()]); + } + + /** + * A FilterInputStream that wraps HTTP streams, with given content-length. + */ + protected static class FixedLengthInputStream extends FilterInputStream { + + private int mBytesLeft = 0; + + protected FixedLengthInputStream(InputStream pIS, int pLength) { + super(pIS); + mBytesLeft = pLength; + } + + public int available() throws IOException { + int available = in.available(); + + return ((available < mBytesLeft) + ? available + : mBytesLeft); + } + + public int read() throws IOException { + if (mBytesLeft-- > 0) { + return in.read(); + } + return -1; + } + + public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { + int read; + + if (mBytesLeft <= 0) { + return -1; // EOF + } + else if (mBytesLeft < pLength) { + + // Read all available + read = in.read(pBytes, pOffset, mBytesLeft); + + //System.err.println("Reading partial: " + read); + mBytesLeft -= read; + return read; + } + + // Just read + read = in.read(pBytes, pOffset, pLength); + + //System.err.println("Reading all avail: " + read); + mBytesLeft -= read; + return read; + } + } + + /** + * A FilterInputStream that wraps HTTP 1.1 "chunked" transfer mode. + */ + protected static class ChunkedInputStream extends FilterInputStream { + + private int mAvailableInCurrentChunk = 0; + + /** + * Creates an input streams that removes the "chunk-headers" and + * makes it look like any other input stream. + */ + protected ChunkedInputStream(InputStream pIS) { + + super(pIS); + if (pIS == null) { + throw new IllegalArgumentException("InputStream may not be null!"); + } + } + + /** + * Returns the number of bytes that can be read from this input stream + * without blocking. + *

+ * This version returns whatever is less of in.available() and the + * length of the current chunk. + * + * @return the number of bytes that can be read from the input stream + * without blocking. + * @throws IOException if an I/O error occurs. + * @see #in + */ + public int available() throws IOException { + + if (mAvailableInCurrentChunk == 0) { + mAvailableInCurrentChunk = parseChunkSize(); + } + int realAvail = in.available(); + + return (mAvailableInCurrentChunk < realAvail) + ? mAvailableInCurrentChunk + : realAvail; + } + + /** + * Reads up to len bytes of data from this input stream into an array + * of bytes. This method blocks until some input is available. + *

+ * This version will read up to len bytes of data, or as much as is + * available in the current chunk. If there is no more data in the + * curernt chunk, the method will read the size of the next chunk, and + * read from that, until the last chunk is read (a chunk with a size of + * 0). + * + * @param pBytes the buffer into which the data is read. + * @param pOffset the start offset of the data. + * @param pLength the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or -1 if + * there is no more data because the end of the stream has been + * reached. + * @throws IOException if an I/O error occurs. + * @see #in + */ + public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { + + //System.err.println("Avail: " + mAvailableInCurrentChunk + // + " length: " + pLength); + int read; + + if (mAvailableInCurrentChunk == -1) { + return -1; // EOF + } + if (mAvailableInCurrentChunk == 0) { + + //System.err.println("Nothing to read, parsing size!"); + // If nothing is read so far, read chunk header + mAvailableInCurrentChunk = parseChunkSize(); + return read(pBytes, pOffset, pLength); + } + else if (mAvailableInCurrentChunk < pLength) { + + // Read all available + read = in.read(pBytes, pOffset, mAvailableInCurrentChunk); + + //System.err.println("Reading partial: " + read); + mAvailableInCurrentChunk -= read; + return read; + } + + // Just read + read = in.read(pBytes, pOffset, pLength); + + //System.err.println("Reading all avail: " + read); + mAvailableInCurrentChunk -= read; + return read; + } + + /** + * Reads the next byte of data from this input stream. The value byte + * is returned as an int in the range 0 to 255. If no byte is available + * because the end of the stream has been reached, the value -1 is + * returned. This method blocks until input data is available, the end + * of the stream is detected, or an exception is thrown. + *

+ * This version reads one byte of data from the current chunk as long + * as there is more data in the chunk. If there is no more data in the + * curernt chunk, the method will read the size of the next chunk, and + * read from that, until the last chunk is read (a chunk with a size of + * 0). + * + * @return the next byte of data, or -1 if the end of the stream is + * reached. + * @see #in + */ + public int read() throws IOException { + + // We have no data, parse chunk header + if (mAvailableInCurrentChunk == -1) { + return -1; + } + else if (mAvailableInCurrentChunk == 0) { + + // Next chunk! + mAvailableInCurrentChunk = parseChunkSize(); + return read(); + } + mAvailableInCurrentChunk--; + return in.read(); + } + + /** + * Reads the chunk size from the chunk header + * {@code chunk-size [SP chunk-extension] CRLF}. + * The chunk-extension is simply discarded. + * + * @return the length of the current chunk, or -1 if the current chunk + * is the last-chunk (a chunk with the size of 0). + */ + protected int parseChunkSize() throws IOException { + + StringBuilder buf = new StringBuilder(); + int b; + + // read chunk-size, chunk-extension (if any) and CRLF + while ((b = in.read()) > 0) { + if ((b == '\r') && (in.read() == '\n')) { // Should be no CR or LF + break; // except for this one... + } + buf.append((char) b); + } + String line = buf.toString(); + + // Happens, as we don't read CRLF off the end of the chunk data... + if (line.length() == 0) { + return 0; + } + + // Discard any chunk-extensions, and read size (HEX). + int spIdx = line.indexOf(' '); + int size = Integer.parseInt(((spIdx >= 0) + ? line.substring(0, spIdx) + : line), 16); + + // This is the last chunk (=EOF) + if (size == 0) { + return -1; + } + return size; + } + } +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/NetUtil.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/NetUtil.java similarity index 85% rename from common/common-io/src/main/java/com/twelvemonkeys/net/NetUtil.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/NetUtil.java index ae7faefd..09d94938 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/NetUtil.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/NetUtil.java @@ -1,1422 +1,1258 @@ -package com.twelvemonkeys.net; - -import com.twelvemonkeys.io.FileUtil; -import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.lang.DateUtil; -import com.twelvemonkeys.util.CollectionUtil; - -import java.io.*; -import java.net.*; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.*; - -/** - * Utility class with network related methods. - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/NetUtil.java#2 $ - */ -public final class NetUtil { - - private final static String VERSION_ID = "NetUtil/2.1"; - - private static Authenticator sAuthenticator = null; - - private final static int BUF_SIZE = 8192; - private final static String HTTP = "http://"; - private final static String HTTPS = "https://"; - - /** - * Field HTTP_PROTOCOL - */ - public final static String HTTP_PROTOCOL = "http"; - - /** - * Field HTTPS_PROTOCOL - */ - public final static String HTTPS_PROTOCOL = "https"; - - /** - * Field HTTP_GET - */ - public final static String HTTP_GET = "GET"; - - /** - * Field HTTP_POST - */ - public final static String HTTP_POST = "POST"; - - /** - * Field HTTP_HEAD - */ - public final static String HTTP_HEAD = "HEAD"; - - /** - * Field HTTP_OPTIONS - */ - public final static String HTTP_OPTIONS = "OPTIONS"; - - /** - * Field HTTP_PUT - */ - public final static String HTTP_PUT = "PUT"; - - /** - * Field HTTP_DELETE - */ - public final static String HTTP_DELETE = "DELETE"; - - /** - * Field HTTP_TRACE - */ - public final static String HTTP_TRACE = "TRACE"; - - /** - * RFC 1123 date format, as reccomended by RFC 2616 (HTTP/1.1), sec 3.3 - * NOTE: All date formats are private, to ensure synchronized access. - */ - private static final SimpleDateFormat HTTP_RFC1123_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); - static { - HTTP_RFC1123_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); - } - - /** - * RFC 850 date format, (allmost) as described in RFC 2616 (HTTP/1.1), sec 3.3 - * USE FOR PARSING ONLY (format is not 100% correct, to be more robust). - */ - private static final SimpleDateFormat HTTP_RFC850_FORMAT = new SimpleDateFormat("EEE, dd-MMM-yy HH:mm:ss z", Locale.US); - /** - * ANSI C asctime() date format, (allmost) as described in RFC 2616 (HTTP/1.1), sec 3.3. - * USE FOR PARSING ONLY (format is not 100% correct, to be more robust). - */ - private static final SimpleDateFormat HTTP_ASCTIME_FORMAT = new SimpleDateFormat("EEE MMM d HH:mm:ss yy", Locale.US); - - private static long sNext50YearWindowChange = DateUtil.currentTimeDay(); - static { - HTTP_RFC850_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); - HTTP_ASCTIME_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); - - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.3: - // - HTTP/1.1 clients and caches SHOULD assume that an RFC-850 date - // which appears to be more than 50 years in the future is in fact - // in the past (this helps solve the "year 2000" problem). - update50YearWindowIfNeeded(); - } - - private static void update50YearWindowIfNeeded() { - // Avoid class synchronization - long next = sNext50YearWindowChange; - - if (next < System.currentTimeMillis()) { - // Next check in one day - next += DateUtil.DAY; - sNext50YearWindowChange = next; - - Date startDate = new Date(next - (50l * DateUtil.CALENDAR_YEAR)); - //System.out.println("next test: " + new Date(next) + ", 50 year start: " + startDate); - synchronized (HTTP_RFC850_FORMAT) { - HTTP_RFC850_FORMAT.set2DigitYearStart(startDate); - } - synchronized (HTTP_ASCTIME_FORMAT) { - HTTP_ASCTIME_FORMAT.set2DigitYearStart(startDate); - } - } - } - - /** - * Creates a NetUtil. - * This class has only static methods and members, and should not be - * instantiated. - */ - private NetUtil() { - } - - public static void main1(String[] args) { - String timeStr = (args.length > 0 && !StringUtil.isNumber(args[0])) ? args[0] : null; - - long time = args.length > 0 ? - (timeStr != null ? parseHTTPDate(timeStr) : Long.parseLong(args[0])) - : System.currentTimeMillis(); - System.out.println(timeStr + " --> " + time + " --> " + formatHTTPDate(time)); - } - - /** - * Main method, reads data from a URL and, optionally, writes it to stdout or a file. - * @param pArgs command line arguemnts - * @throws java.io.IOException if an I/O exception occurs - */ - public static void main(String[] pArgs) throws IOException { - // params: - int timeout = 0; - boolean followRedirects = true; - boolean debugHeaders = false; - String requestPropertiesFile = null; - String requestHeaders = null; - String postData = null; - File putData = null; - int argIdx = 0; - boolean errArgs = false; - boolean writeToFile = false; - boolean writeToStdOut = false; - String outFileName = null; - - while ((argIdx < pArgs.length) && (pArgs[argIdx].charAt(0) == '-') && (pArgs[argIdx].length() >= 2)) { - if ((pArgs[argIdx].charAt(1) == 't') || pArgs[argIdx].equals("--timeout")) { - argIdx++; - try { - timeout = Integer.parseInt(pArgs[argIdx++]); - } - catch (NumberFormatException nfe) { - errArgs = true; - break; - } - } - else if ((pArgs[argIdx].charAt(1) == 'd') || pArgs[argIdx].equals("--debugheaders")) { - debugHeaders = true; - argIdx++; - } - else if ((pArgs[argIdx].charAt(1) == 'n') || pArgs[argIdx].equals("--nofollowredirects")) { - followRedirects = false; - argIdx++; - } - else if ((pArgs[argIdx].charAt(1) == 'r') || pArgs[argIdx].equals("--requestproperties")) { - argIdx++; - requestPropertiesFile = pArgs[argIdx++]; - } - else if ((pArgs[argIdx].charAt(1) == 'p') || pArgs[argIdx].equals("--postdata")) { - argIdx++; - postData = pArgs[argIdx++]; - } - else if ((pArgs[argIdx].charAt(1) == 'u') || pArgs[argIdx].equals("--putdata")) { - argIdx++; - putData = new File(pArgs[argIdx++]); - if (!putData.exists()) { - errArgs = true; - break; - } - } - else if ((pArgs[argIdx].charAt(1) == 'h') || pArgs[argIdx].equals("--header")) { - argIdx++; - requestHeaders = pArgs[argIdx++]; - } - else if ((pArgs[argIdx].charAt(1) == 'f') || pArgs[argIdx].equals("--file")) { - argIdx++; - writeToFile = true; - - // Get optional file name - if (!((argIdx >= (pArgs.length - 1)) || (pArgs[argIdx].charAt(0) == '-'))) { - outFileName = pArgs[argIdx++]; - } - } - else if ((pArgs[argIdx].charAt(1) == 'o') || pArgs[argIdx].equals("--output")) { - argIdx++; - writeToStdOut = true; - } - else { - System.err.println("Unknown option \"" + pArgs[argIdx++] + "\""); - } - } - if (errArgs || (pArgs.length < (argIdx + 1))) { - System.err.println("Usage: java NetUtil [-f|--file []] [-d|--debugheaders] [-h|--header

] [-p|--postdata ] [-u|--putdata ] [-r|--requestProperties ] [-t|--timeout ] [-n|--nofollowredirects] fromUrl"); - System.exit(5); - } - String url = pArgs[argIdx/*++*/]; - - // DONE ARGS - // Get request properties - Properties requestProperties = new Properties(); - - if (requestPropertiesFile != null) { - - // Just read, no exception handling... - requestProperties.load(new FileInputStream(new File(requestPropertiesFile))); - } - if (requestHeaders != null) { - - // Get request headers - String[] headerPairs = StringUtil.toStringArray(requestHeaders, ","); - - for (String headerPair : headerPairs) { - String[] pair = StringUtil.toStringArray(headerPair, ":"); - String key = (pair.length > 0) - ? pair[0].trim() - : null; - String value = (pair.length > 1) - ? pair[1].trim() - : ""; - - if (key != null) { - requestProperties.setProperty(key, value); - } - } - } - java.net.HttpURLConnection conn; - - // Create connection - URL reqURL = getURLAndSetAuthorization(url, requestProperties); - - conn = createHttpURLConnection(reqURL, requestProperties, followRedirects, timeout); - - // POST - if (postData != null) { - // HTTP POST method - conn.setRequestMethod(HTTP_POST); - - // Set entity headers - conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - conn.setRequestProperty("Content-Length", String.valueOf(postData.length())); - conn.setRequestProperty("Content-Encoding", "ISO-8859-1"); - - // Get outputstream (this is where the connect actually happens) - OutputStream os = conn.getOutputStream(); - - System.err.println("OutputStream: " + os.getClass().getName() + "@" + System.identityHashCode(os)); - OutputStreamWriter writer = new OutputStreamWriter(os, "ISO-8859-1"); - - // Write post data to the stream - writer.write(postData); - writer.write("\r\n"); - - //writer.flush(); - writer.close(); // Does this close the underlying stream? - } - // PUT - else if (putData != null) { - // HTTP PUT method - conn.setRequestMethod(HTTP_PUT); - - // Set entity headers - //conn.setRequestProperty("Content-Type", "???"); - // TODO: Set Content-Type to correct type? - // TODO: Set content-encoding? Or can binary data be sent directly? - conn.setRequestProperty("Content-Length", String.valueOf(putData.length())); - - // Get outputstream (this is where the connect actually happens) - OutputStream os = conn.getOutputStream(); - - System.err.println("OutputStream: " + os.getClass().getName() + "@" + System.identityHashCode(os)); - - // Write put data to the stream - FileUtil.copy(new FileInputStream(putData), os); - - os.close(); - } - - // - InputStream is; - - if (conn.getResponseCode() == 200) { - - // Connect and get stream - is = conn.getInputStream(); - } - else { - is = conn.getErrorStream(); - } - - // - if (debugHeaders) { - System.err.println("Request (debug):"); - System.err.println(conn.getClass()); - System.err.println("Response (debug):"); - - // Headerfield 0 is response code - System.err.println(conn.getHeaderField(0)); - - // Loop from 1, as headerFieldKey(0) == null... - for (int i = 1; ; i++) { - String key = conn.getHeaderFieldKey(i); - - // Seems to be the way to loop through them all... - if (key == null) { - break; - } - System.err.println(key + ": " + conn.getHeaderField(key)); - } - } - - // Create output file if specified - OutputStream os; - - if (writeToFile) { - if (outFileName == null) { - outFileName = reqURL.getFile(); - if (StringUtil.isEmpty(outFileName)) { - outFileName = conn.getHeaderField("Location"); - if (StringUtil.isEmpty(outFileName)) { - outFileName = "index"; - - // Find a suitable extension - // TODO: Replace with MIME-type util with MIME/file ext mapping - String ext = conn.getContentType(); - - if (!StringUtil.isEmpty(ext)) { - int idx = ext.lastIndexOf('/'); - - if (idx >= 0) { - ext = ext.substring(idx + 1); - } - idx = ext.indexOf(';'); - if (idx >= 0) { - ext = ext.substring(0, idx); - } - outFileName += "." + ext; - } - } - } - int idx = outFileName.lastIndexOf('/'); - - if (idx >= 0) { - outFileName = outFileName.substring(idx + 1); - } - idx = outFileName.indexOf('?'); - if (idx >= 0) { - outFileName = outFileName.substring(0, idx); - } - } - File outFile = new File(outFileName); - - if (!outFile.createNewFile()) { - if (outFile.exists()) { - System.err.println("Cannot write to file " + outFile.getAbsolutePath() + ", file allready exists."); - } - else { - System.err.println("Cannot write to file " + outFile.getAbsolutePath() + ", check write permissions."); - } - System.exit(5); - } - os = new FileOutputStream(outFile); - } - else if (writeToStdOut) { - os = System.out; - } - else { - os = null; - } - - // Get data. - if ((writeToFile || writeToStdOut) && is != null) { - FileUtil.copy(is, os); - } - - /* - Hashtable postData = new Hashtable(); - postData.put("SearchText", "condition"); - - try { - InputStream in = getInputStreamHttpPost(pArgs[argIdx], postData, - props, true, 0); - out = new FileOutputStream(file); - FileUtil.copy(in, out); - } - catch (Exception e) { - System.err.println("Error: " + e); - e.printStackTrace(System.err); - continue; - } - */ - } - - /* - public static class Cookie { - String mName = null; - String mValue = null; - - public Cookie(String pName, String pValue) { - mName = pName; - mValue = pValue; - } - - public String toString() { - return mName + "=" + mValue; - } - */ - - /* - // Just a way to set cookies.. - if (pCookies != null) { - String cookieStr = ""; - for (int i = 0; i < pCookies.length; i++) - cookieStr += ((i == pCookies.length) ? pCookies[i].toString() - : pCookies[i].toString() + ";"); - - // System.out.println("Cookie: " + cookieStr); - - conn.setRequestProperty("Cookie", cookieStr); - } - */ - - /* - } - */ - - /** - * Test if the given URL is using HTTP protocol. - * - * @param pURL the url to condition - * @return true if the protocol is HTTP. - */ - public static boolean isHttpURL(String pURL) { - return ((pURL != null) && pURL.startsWith(HTTP)); - } - - /** - * Test if the given URL is using HTTP protocol. - * - * @param pURL the url to condition - * @return true if the protocol is HTTP. - */ - public static boolean isHttpURL(URL pURL) { - return ((pURL != null) && pURL.getProtocol().equals("http")); - } - - /** - * Gets the content from a given URL, and returns it as a byte array. - * Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * NOTE: If you supply a username and password for HTTP - * authentication, this method uses the java.net.Authenticator's static - * {@code setDefault()} method, that can only be set ONCE. This - * means that if the default Authenticator is allready set, this method - * will fail. - * It also means if any other piece of code tries to register a new default - * Authenticator within the current VM, it will fail. - * - * @param pURL A String containing the URL, on the form - * [http://][:@]servername[/file.ext] - * where everything in brackets are optional. - * @return a byte array with the URL contents. If an error occurs, the - * returned array may be zero-length, but not null. - * @throws MalformedURLException if the urlName parameter is not a valid - * URL. Note that the protocol cannot be anything but HTTP. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see java.net.Authenticator - * @see SimpleAuthenticator - */ - public static byte[] getBytesHttp(String pURL) throws IOException { - return getBytesHttp(pURL, 0); - } - - /** - * Gets the content from a given URL, and returns it as a byte array. - * - * @param pURL the URL to get. - * @return a byte array with the URL contents. If an error occurs, the - * returned array may be zero-length, but not null. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getBytesHttp(String) - */ - public static byte[] getBytesHttp(URL pURL) throws IOException { - return getBytesHttp(pURL, 0); - } - - /** - * Gets the InputStream from a given URL. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * NOTE: If you supply a username and password for HTTP - * authentication, this method uses the java.net.Authenticator's static - * {@code setDefault()} method, that can only be set ONCE. This - * means that if the default Authenticator is allready set, this method - * will fail. - * It also means if any other piece of code tries to register a new default - * Authenticator within the current VM, it will fail. - * - * @param pURL A String containing the URL, on the form - * [http://][:@]servername[/file.ext] - * where everything in brackets are optional. - * @return an input stream that reads from the connection created by the - * given URL. - * @throws MalformedURLException if the urlName parameter specifies an - * unknown protocol, or does not form a valid URL. - * Note that the protocol cannot be anything but HTTP. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see java.net.Authenticator - * @see SimpleAuthenticator - */ - public static InputStream getInputStreamHttp(String pURL) throws IOException { - return getInputStreamHttp(pURL, 0); - } - - /** - * Gets the InputStream from a given URL. - * - * @param pURL the URL to get. - * @return an input stream that reads from the connection created by the - * given URL. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getInputStreamHttp(String) - */ - public static InputStream getInputStreamHttp(URL pURL) throws IOException { - return getInputStreamHttp(pURL, 0); - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws MalformedURLException if the url parameter specifies an - * unknown protocol, or does not form a valid URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getInputStreamHttp(URL,int) - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static InputStream getInputStreamHttp(String pURL, int pTimeout) throws IOException { - return getInputStreamHttp(pURL, null, true, pTimeout); - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pProperties the request header properties. - * @param pFollowRedirects specifying wether redirects should be followed. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws MalformedURLException if the url parameter specifies an - * unknown protocol, or does not form a valid URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getInputStreamHttp(URL,int) - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static InputStream getInputStreamHttp(final String pURL, final Properties pProperties, final boolean pFollowRedirects, final int pTimeout) - throws IOException { - - // Make sure we have properties - Properties properties = pProperties != null ? pProperties : new Properties(); - - //URL url = getURLAndRegisterPassword(pURL); - URL url = getURLAndSetAuthorization(pURL, properties); - - //unregisterPassword(url); - return getInputStreamHttp(url, properties, pFollowRedirects, pTimeout); - } - - /** - * Registers the password from the URL string, and returns the URL object. - * - * @param pURL the string representation of the URL, possibly including authorization part - * @param pProperties the - * @return the URL created from {@code pURL}. - * @throws java.net.MalformedURLException if there's a syntax error in {@code pURL} - */ - private static URL getURLAndSetAuthorization(final String pURL, final Properties pProperties) throws MalformedURLException { - String url = pURL; - // Split user/password away from url - String userPass = null; - String protocolPrefix = HTTP; - int httpIdx = url.indexOf(HTTPS); - - if (httpIdx >= 0) { - protocolPrefix = HTTPS; - url = url.substring(httpIdx + HTTPS.length()); - } - else { - httpIdx = url.indexOf(HTTP); - if (httpIdx >= 0) { - url = url.substring(httpIdx + HTTP.length()); - } - } - - // Get authorization part - int atIdx = url.indexOf("@"); - - if (atIdx >= 0) { - userPass = url.substring(0, atIdx); - url = url.substring(atIdx + 1); - } - - // Set authorization if user/password is present - if (userPass != null) { - // System.out.println("Setting password ("+ userPass + ")!"); - pProperties.setProperty("Authorization", "Basic " + BASE64.encode(userPass.getBytes())); - } - - // Return URL - return new URL(protocolPrefix + url); - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see com.twelvemonkeys.net.HttpURLConnection - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.net.HttpURLConnection - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static InputStream getInputStreamHttp(URL pURL, int pTimeout) throws IOException { - return getInputStreamHttp(pURL, null, true, pTimeout); - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pProperties the request header properties. - * @param pFollowRedirects specifying wether redirects should be followed. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getInputStreamHttp(URL,int) - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static InputStream getInputStreamHttp(URL pURL, Properties pProperties, boolean pFollowRedirects, int pTimeout) - throws IOException { - - // Open the connection, and get the stream - java.net.HttpURLConnection conn = createHttpURLConnection(pURL, pProperties, pFollowRedirects, pTimeout); - - // HTTP GET method - conn.setRequestMethod(HTTP_GET); - - // This is where the connect happens - InputStream is = conn.getInputStream(); - - // We only accept the 200 OK message - if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { - throw new IOException("The request gave the response: " + conn.getResponseCode() + ": " + conn.getResponseMessage()); - } - return is; - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pPostData the post data. - * @param pProperties the request header properties. - * @param pFollowRedirects specifying wether redirects should be followed. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws MalformedURLException if the url parameter specifies an - * unknown protocol, or does not form a valid URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - */ - public static InputStream getInputStreamHttpPost(String pURL, Map pPostData, Properties pProperties, boolean pFollowRedirects, int pTimeout) - throws IOException { - - pProperties = pProperties != null ? pProperties : new Properties(); - - //URL url = getURLAndRegisterPassword(pURL); - URL url = getURLAndSetAuthorization(pURL, pProperties); - - //unregisterPassword(url); - return getInputStreamHttpPost(url, pPostData, pProperties, pFollowRedirects, pTimeout); - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pPostData the post data. - * @param pProperties the request header properties. - * @param pFollowRedirects specifying wether redirects should be followed. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - */ - public static InputStream getInputStreamHttpPost(URL pURL, Map pPostData, Properties pProperties, boolean pFollowRedirects, int pTimeout) - throws IOException { - // Open the connection, and get the stream - java.net.HttpURLConnection conn = createHttpURLConnection(pURL, pProperties, pFollowRedirects, pTimeout); - - // HTTP POST method - conn.setRequestMethod(HTTP_POST); - - // Iterate over and create post data string - StringBuilder postStr = new StringBuilder(); - - if (pPostData != null) { - Iterator data = pPostData.entrySet().iterator(); - - while (data.hasNext()) { - Map.Entry entry = (Map.Entry) data.next(); - - // Properties key/values can be safely cast to strings - // Encode the string - postStr.append(URLEncoder.encode((String) entry.getKey(), "UTF-8")); - postStr.append('='); - postStr.append(URLEncoder.encode(entry.getValue().toString(), "UTF-8")); - - if (data.hasNext()) { - postStr.append('&'); - } - } - } - - // Set entity headers - String encoding = conn.getRequestProperty("Content-Encoding"); - if (StringUtil.isEmpty(encoding)) { - encoding = "UTF-8"; - } - conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - conn.setRequestProperty("Content-Length", String.valueOf(postStr.length())); - conn.setRequestProperty("Content-Encoding", encoding); - - // Get outputstream (this is where the connect actually happens) - OutputStream os = conn.getOutputStream(); - OutputStreamWriter writer = new OutputStreamWriter(os, encoding); - - // Write post data to the stream - writer.write(postStr.toString()); - writer.write("\r\n"); - writer.close(); // Does this close the underlying stream? - - // Get the inputstream - InputStream is = conn.getInputStream(); - - // We only accept the 200 OK message - // TODO: Accept all 200 messages, like ACCEPTED, CREATED or NO_CONTENT? - if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { - throw new IOException("The request gave the response: " + conn.getResponseCode() + ": " + conn.getResponseMessage()); - } - return is; - } - - /** - * Creates a HTTP connection to the given URL. - * - * @param pURL the URL to get. - * @param pProperties connection properties. - * @param pFollowRedirects specifies whether we should follow redirects. - * @param pTimeout the specified timeout, in milliseconds. - * @return a HttpURLConnection - * @throws UnknownHostException if the hostname in the URL cannot be found. - * @throws IOException if an I/O exception occurs. - */ - public static java.net.HttpURLConnection createHttpURLConnection(URL pURL, Properties pProperties, boolean pFollowRedirects, int pTimeout) - throws IOException { - - // Open the connection, and get the stream - java.net.HttpURLConnection conn; - - if (pTimeout > 0) { - // Supports timeout - conn = new com.twelvemonkeys.net.HttpURLConnection(pURL, pTimeout); - } - else { - // Faster, more compatible - conn = (java.net.HttpURLConnection) pURL.openConnection(); - } - - // Set user agent - if ((pProperties == null) || !pProperties.containsKey("User-Agent")) { - conn.setRequestProperty("User-Agent", - VERSION_ID - + " (" + System.getProperty("os.name") + "/" + System.getProperty("os.version") + "; " - + System.getProperty("os.arch") + "; " - + System.getProperty("java.vm.name") + "/" + System.getProperty("java.vm.version") + ")"); - } - - // Set request properties - if (pProperties != null) { - for (Map.Entry entry : pProperties.entrySet()) { - // Properties key/values can be safely cast to strings - conn.setRequestProperty((String) entry.getKey(), entry.getValue().toString()); - } - } - - try { - // Breaks with JRE1.2? - conn.setInstanceFollowRedirects(pFollowRedirects); - } - catch (LinkageError le) { - // This is the best we can do... - java.net.HttpURLConnection.setFollowRedirects(pFollowRedirects); - System.err.println("You are using an old Java Spec, consider upgrading."); - System.err.println("java.net.HttpURLConnection.setInstanceFollowRedirects(" + pFollowRedirects + ") failed."); - - //le.printStackTrace(System.err); - } - - conn.setDoInput(true); - conn.setDoOutput(true); - - //conn.setUseCaches(true); - return conn; - } - - /** - * This is a hack to get around the protected constructors in - * HttpURLConnection, should maybe consider registering and do things - * properly... - */ - - /* - private static class TimedHttpURLConnection - extends com.twelvemonkeys.net.HttpURLConnection { - TimedHttpURLConnection(URL pURL, int pTimeout) { - super(pURL, pTimeout); - } - } - */ - - /** - * Gets the content from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. - *
- *
- * - * @param pURL the URL to get. - * @param pTimeout the specified timeout, in milliseconds. - * @return a byte array that is read from the socket connection, created - * from the given URL. - * @throws MalformedURLException if the url parameter specifies an - * unknown protocol, or does not form a valid URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getBytesHttp(URL,int) - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static byte[] getBytesHttp(String pURL, int pTimeout) throws IOException { - // Get the input stream from the url - InputStream in = new BufferedInputStream(getInputStreamHttp(pURL, pTimeout), BUF_SIZE * 2); - - // Get all the bytes in loop - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - int count; - byte[] buffer = new byte[BUF_SIZE]; - - try { - while ((count = in.read(buffer)) != -1) { - // NOTE: According to the J2SE API doc, read(byte[]) will read - // at least 1 byte, or return -1, if end-of-file is reached. - bytes.write(buffer, 0, count); - } - } - finally { - - // Close the buffer - in.close(); - } - return bytes.toByteArray(); - } - - /** - * Gets the content from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. - *
- *
- * - * @param pURL the URL to get. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getInputStreamHttp(URL,int) - * @see com.twelvemonkeys.net.HttpURLConnection - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.net.HttpURLConnection - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static byte[] getBytesHttp(URL pURL, int pTimeout) throws IOException { - // Get the input stream from the url - InputStream in = new BufferedInputStream(getInputStreamHttp(pURL, pTimeout), BUF_SIZE * 2); - - // Get all the bytes in loop - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - int count; - byte[] buffer = new byte[BUF_SIZE]; - - try { - while ((count = in.read(buffer)) != -1) { - // NOTE: According to the J2SE API doc, read(byte[]) will read - // at least 1 byte, or return -1, if end-of-file is reached. - bytes.write(buffer, 0, count); - } - } - finally { - - // Close the buffer - in.close(); - } - return bytes.toByteArray(); - } - - /** - * Unregisters the password asscociated with this URL - */ - - /* - private static void unregisterPassword(URL pURL) { - Authenticator auth = registerAuthenticator(); - if (auth != null && auth instanceof SimpleAuthenticator) - ((SimpleAuthenticator) auth) - .unregisterPasswordAuthentication(pURL); - } - */ - - /** - * Registers the password from the URL string, and returns the URL object. - */ - - /* - private static URL getURLAndRegisterPassword(String pURL) - throws MalformedURLException - { - // Split user/password away from url - String userPass = null; - String protocolPrefix = HTTP; - - int httpIdx = pURL.indexOf(HTTPS); - if (httpIdx >= 0) { - protocolPrefix = HTTPS; - pURL = pURL.substring(httpIdx + HTTPS.length()); - } - else { - httpIdx = pURL.indexOf(HTTP); - if (httpIdx >= 0) - pURL = pURL.substring(httpIdx + HTTP.length()); - } - - int atIdx = pURL.indexOf("@"); - if (atIdx >= 0) { - userPass = pURL.substring(0, atIdx); - pURL = pURL.substring(atIdx + 1); - } - - // Set URL - URL url = new URL(protocolPrefix + pURL); - - // Set Authenticator if user/password is present - if (userPass != null) { - // System.out.println("Setting password ("+ userPass + ")!"); - - int colIdx = userPass.indexOf(":"); - if (colIdx < 0) - throw new MalformedURLException("Error in username/password!"); - - String userName = userPass.substring(0, colIdx); - String passWord = userPass.substring(colIdx + 1); - - // Try to register the authenticator - // System.out.println("Trying to register authenticator!"); - Authenticator auth = registerAuthenticator(); - - // System.out.println("Got authenticator " + auth + "."); - - // Register our username/password with it - if (auth != null && auth instanceof SimpleAuthenticator) { - ((SimpleAuthenticator) auth) - .registerPasswordAuthentication(url, - new PasswordAuthentication(userName, - passWord.toCharArray())); - } - else { - // Not supported! - throw new RuntimeException("Could not register PasswordAuthentication"); - } - } - - return url; - } - */ - - /** - * Registers the Authenticator given in the system property - * {@code java.net.Authenticator}, or the default implementation - * ({@code com.twelvemonkeys.net.SimpleAuthenticator}). - *

- * BUG: What if authenticator has allready been set outside this class? - * - * @return The Authenticator created and set as default, or null, if it - * was not set as the default. However, there is no (clean) way to - * be sure the authenticator was set (the SimpleAuthenticator uses - * a hack to get around this), so it might be possible that the - * returned authenticator was not set as default... - * @see Authenticator#setDefault(Authenticator) - * @see SimpleAuthenticator - */ - public synchronized static Authenticator registerAuthenticator() { - if (sAuthenticator != null) { - return sAuthenticator; - } - - // Get the system property - String authenticatorName = System.getProperty("java.net.Authenticator"); - - // Try to get the Authenticator from the system property - if (authenticatorName != null) { - try { - Class authenticatorClass = Class.forName(authenticatorName); - - sAuthenticator = (Authenticator) authenticatorClass.newInstance(); - } - catch (ClassNotFoundException cnfe) { - // We should maybe rethrow this? - } - catch (InstantiationException ie) { - // Ignore - } - catch (IllegalAccessException iae) { - // Ignore - } - } - - // Get the default authenticator - if (sAuthenticator == null) { - sAuthenticator = SimpleAuthenticator.getInstance(); - } - - // Register authenticator as default - Authenticator.setDefault(sAuthenticator); - return sAuthenticator; - } - - /** - * Creates the InetAddress object from the given URL. - * Equivalent to calling {@code InetAddress.getByName(URL.getHost())} - * except that it returns null, instead of throwing UnknownHostException. - * - * @param pURL the URL to look up. - * @return the createad InetAddress, or null if the host was unknown. - * @see java.net.InetAddress - * @see java.net.URL - */ - public static InetAddress createInetAddressFromURL(URL pURL) { - try { - return InetAddress.getByName(pURL.getHost()); - } - catch (UnknownHostException e) { - return null; - } - } - - /** - * Creates an URL from the given InetAddress object, using the given - * protocol. - * Equivalent to calling - * {@code new URL(protocol, InetAddress.getHostName(), "")} - * except that it returns null, instead of throwing MalformedURLException. - * - * @param pIP the IP address to look up - * @param pProtocol the protocol to use in the new URL - * @return the created URL or null, if the URL could not be created. - * @see java.net.URL - * @see java.net.InetAddress - */ - public static URL createURLFromInetAddress(InetAddress pIP, String pProtocol) { - try { - return new URL(pProtocol, pIP.getHostName(), ""); - } - catch (MalformedURLException e) { - return null; - } - } - - /** - * Creates an URL from the given InetAddress object, using HTTP protocol. - * Equivalent to calling - * {@code new URL("http", InetAddress.getHostName(), "")} - * except that it returns null, instead of throwing MalformedURLException. - * - * @param pIP the IP address to look up - * @return the created URL or null, if the URL could not be created. - * @see java.net.URL - * @see java.net.InetAddress - */ - public static URL createURLFromInetAddress(InetAddress pIP) { - return createURLFromInetAddress(pIP, HTTP); - } - - /* - * TODO: Benchmark! - */ - static byte[] getBytesHttpOld(String pURL) throws IOException { - // Get the input stream from the url - InputStream in = new BufferedInputStream(getInputStreamHttp(pURL), BUF_SIZE * 2); - - // Get all the bytes in loop - byte[] bytes = new byte[0]; - int count; - byte[] buffer = new byte[BUF_SIZE]; - - try { - while ((count = in.read(buffer)) != -1) { - - // NOTE: According to the J2SE API doc, read(byte[]) will read - // at least 1 byte, or return -1, if end-of-file is reached. - bytes = (byte[]) CollectionUtil.mergeArrays(bytes, 0, bytes.length, buffer, 0, count); - } - } - finally { - - // Close the buffer - in.close(); - } - return bytes; - } - - /** - * Formats the time to a HTTP date, using the RFC 1123 format, as described - * in RFC 2616 (HTTP/1.1), sec. 3.3. - * - * @param pTime the time - * @return a {@code String} representation of the time - */ - public static String formatHTTPDate(long pTime) { - return formatHTTPDate(new Date(pTime)); - } - - /** - * Formats the time to a HTTP date, using the RFC 1123 format, as described - * in RFC 2616 (HTTP/1.1), sec. 3.3. - * - * @param pTime the time - * @return a {@code String} representation of the time - */ - public static String formatHTTPDate(Date pTime) { - synchronized (HTTP_RFC1123_FORMAT) { - return HTTP_RFC1123_FORMAT.format(pTime); - } - } - - /** - * Parses a HTTP date string into a {@code long} representing milliseconds - * since January 1, 1970 GMT. - *

- * Use this method with headers that contain dates, such as - * {@code If-Modified-Since} or {@code Last-Modified}. - *

- * The date string may be in either RFC 1123, RFC 850 or ANSI C asctime() - * format, as described in - * RFC 2616 (HTTP/1.1), sec. 3.3 - * - * @param pDate the date to parse - * - * @return a {@code long} value representing the date, expressed as the - * number of milliseconds since January 1, 1970 GMT, - * @throws NumberFormatException if the date parameter is not parseable. - * @throws IllegalArgumentException if the date paramter is {@code null} - */ - public static long parseHTTPDate(String pDate) throws NumberFormatException { - return parseHTTPDateImpl(pDate).getTime(); - } - - /** - * ParseHTTPDate implementation - * - * @param pDate the date string to parse - * - * @return a {@code Date} - * @throws NumberFormatException if the date parameter is not parseable. - * @throws IllegalArgumentException if the date paramter is {@code null} - */ - private static Date parseHTTPDateImpl(final String pDate) throws NumberFormatException { - if (pDate == null) { - throw new IllegalArgumentException("date == null"); - } - - if (StringUtil.isEmpty(pDate)) { - throw new NumberFormatException("Invalid HTTP date: \"" + pDate + "\""); - } - - DateFormat format; - - if (pDate.indexOf('-') >= 0) { - format = HTTP_RFC850_FORMAT; - update50YearWindowIfNeeded(); - } - else if (pDate.indexOf(',') < 0) { - format = HTTP_ASCTIME_FORMAT; - update50YearWindowIfNeeded(); - } - else { - format = HTTP_RFC1123_FORMAT; - // NOTE: RFC1123 always uses 4-digit years - } - - Date date; - try { - //noinspection SynchronizationOnLocalVariableOrMethodParameter - synchronized (format) { - date = format.parse(pDate); - } - } - catch (ParseException e) { - NumberFormatException nfe = new NumberFormatException("Invalid HTTP date: \"" + pDate + "\""); - nfe.initCause(e); - throw nfe; - } - - if (date == null) { - throw new NumberFormatException("Invalid HTTP date: \"" + pDate + "\""); - } - - return date; - } +package com.twelvemonkeys.net; + +import com.twelvemonkeys.io.FileUtil; +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.util.CollectionUtil; + +import java.io.*; +import java.net.*; +import java.net.HttpURLConnection; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; + +/** + * Utility class with network related methods. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/NetUtil.java#2 $ + */ +public final class NetUtil { + + private final static String VERSION_ID = "NetUtil/2.1"; + + private static Authenticator sAuthenticator = null; + + private final static int BUF_SIZE = 8192; + private final static String HTTP = "http://"; + private final static String HTTPS = "https://"; + + /** + * Field HTTP_PROTOCOL + */ + public final static String HTTP_PROTOCOL = "http"; + + /** + * Field HTTPS_PROTOCOL + */ + public final static String HTTPS_PROTOCOL = "https"; + + /** + * Field HTTP_GET + */ + public final static String HTTP_GET = "GET"; + + /** + * Field HTTP_POST + */ + public final static String HTTP_POST = "POST"; + + /** + * Field HTTP_HEAD + */ + public final static String HTTP_HEAD = "HEAD"; + + /** + * Field HTTP_OPTIONS + */ + public final static String HTTP_OPTIONS = "OPTIONS"; + + /** + * Field HTTP_PUT + */ + public final static String HTTP_PUT = "PUT"; + + /** + * Field HTTP_DELETE + */ + public final static String HTTP_DELETE = "DELETE"; + + /** + * Field HTTP_TRACE + */ + public final static String HTTP_TRACE = "TRACE"; + + /** + * Creates a NetUtil. + * This class has only static methods and members, and should not be + * instantiated. + */ + private NetUtil() { + } + + /** + * Main method, reads data from a URL and, optionally, writes it to stdout or a file. + * @param pArgs command line arguemnts + * @throws java.io.IOException if an I/O exception occurs + */ + public static void main(String[] pArgs) throws IOException { + // params: + int timeout = 0; + boolean followRedirects = true; + boolean debugHeaders = false; + String requestPropertiesFile = null; + String requestHeaders = null; + String postData = null; + File putData = null; + int argIdx = 0; + boolean errArgs = false; + boolean writeToFile = false; + boolean writeToStdOut = false; + String outFileName = null; + + while ((argIdx < pArgs.length) && (pArgs[argIdx].charAt(0) == '-') && (pArgs[argIdx].length() >= 2)) { + if ((pArgs[argIdx].charAt(1) == 't') || pArgs[argIdx].equals("--timeout")) { + argIdx++; + try { + timeout = Integer.parseInt(pArgs[argIdx++]); + } + catch (NumberFormatException nfe) { + errArgs = true; + break; + } + } + else if ((pArgs[argIdx].charAt(1) == 'd') || pArgs[argIdx].equals("--debugheaders")) { + debugHeaders = true; + argIdx++; + } + else if ((pArgs[argIdx].charAt(1) == 'n') || pArgs[argIdx].equals("--nofollowredirects")) { + followRedirects = false; + argIdx++; + } + else if ((pArgs[argIdx].charAt(1) == 'r') || pArgs[argIdx].equals("--requestproperties")) { + argIdx++; + requestPropertiesFile = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'p') || pArgs[argIdx].equals("--postdata")) { + argIdx++; + postData = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'u') || pArgs[argIdx].equals("--putdata")) { + argIdx++; + putData = new File(pArgs[argIdx++]); + if (!putData.exists()) { + errArgs = true; + break; + } + } + else if ((pArgs[argIdx].charAt(1) == 'h') || pArgs[argIdx].equals("--header")) { + argIdx++; + requestHeaders = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'f') || pArgs[argIdx].equals("--file")) { + argIdx++; + writeToFile = true; + + // Get optional file name + if (!((argIdx >= (pArgs.length - 1)) || (pArgs[argIdx].charAt(0) == '-'))) { + outFileName = pArgs[argIdx++]; + } + } + else if ((pArgs[argIdx].charAt(1) == 'o') || pArgs[argIdx].equals("--output")) { + argIdx++; + writeToStdOut = true; + } + else { + System.err.println("Unknown option \"" + pArgs[argIdx++] + "\""); + } + } + if (errArgs || (pArgs.length < (argIdx + 1))) { + System.err.println("Usage: java NetUtil [-f|--file []] [-d|--debugheaders] [-h|--header

] [-p|--postdata ] [-u|--putdata ] [-r|--requestProperties ] [-t|--timeout ] [-n|--nofollowredirects] fromUrl"); + System.exit(5); + } + String url = pArgs[argIdx/*++*/]; + + // DONE ARGS + // Get request properties + Properties requestProperties = new Properties(); + + if (requestPropertiesFile != null) { + + // Just read, no exception handling... + requestProperties.load(new FileInputStream(new File(requestPropertiesFile))); + } + if (requestHeaders != null) { + + // Get request headers + String[] headerPairs = StringUtil.toStringArray(requestHeaders, ","); + + for (String headerPair : headerPairs) { + String[] pair = StringUtil.toStringArray(headerPair, ":"); + String key = (pair.length > 0) + ? pair[0].trim() + : null; + String value = (pair.length > 1) + ? pair[1].trim() + : ""; + + if (key != null) { + requestProperties.setProperty(key, value); + } + } + } + HttpURLConnection conn; + + // Create connection + URL reqURL = getURLAndSetAuthorization(url, requestProperties); + + conn = createHttpURLConnection(reqURL, requestProperties, followRedirects, timeout); + + // POST + if (postData != null) { + // HTTP POST method + conn.setRequestMethod(HTTP_POST); + + // Set entity headers + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("Content-Length", String.valueOf(postData.length())); + conn.setRequestProperty("Content-Encoding", "ISO-8859-1"); + + // Get outputstream (this is where the connect actually happens) + OutputStream os = conn.getOutputStream(); + + System.err.println("OutputStream: " + os.getClass().getName() + "@" + System.identityHashCode(os)); + OutputStreamWriter writer = new OutputStreamWriter(os, "ISO-8859-1"); + + // Write post data to the stream + writer.write(postData); + writer.write("\r\n"); + + //writer.flush(); + writer.close(); // Does this close the underlying stream? + } + // PUT + else if (putData != null) { + // HTTP PUT method + conn.setRequestMethod(HTTP_PUT); + + // Set entity headers + //conn.setRequestProperty("Content-Type", "???"); + // TODO: Set Content-Type to correct type? + // TODO: Set content-encoding? Or can binary data be sent directly? + conn.setRequestProperty("Content-Length", String.valueOf(putData.length())); + + // Get outputstream (this is where the connect actually happens) + OutputStream os = conn.getOutputStream(); + + System.err.println("OutputStream: " + os.getClass().getName() + "@" + System.identityHashCode(os)); + + // Write put data to the stream + FileUtil.copy(new FileInputStream(putData), os); + + os.close(); + } + + // + InputStream is; + + if (conn.getResponseCode() == 200) { + + // Connect and get stream + is = conn.getInputStream(); + } + else { + is = conn.getErrorStream(); + } + + // + if (debugHeaders) { + System.err.println("Request (debug):"); + System.err.println(conn.getClass()); + System.err.println("Response (debug):"); + + // Headerfield 0 is response code + System.err.println(conn.getHeaderField(0)); + + // Loop from 1, as headerFieldKey(0) == null... + for (int i = 1; ; i++) { + String key = conn.getHeaderFieldKey(i); + + // Seems to be the way to loop through them all... + if (key == null) { + break; + } + System.err.println(key + ": " + conn.getHeaderField(key)); + } + } + + // Create output file if specified + OutputStream os; + + if (writeToFile) { + if (outFileName == null) { + outFileName = reqURL.getFile(); + if (StringUtil.isEmpty(outFileName)) { + outFileName = conn.getHeaderField("Location"); + if (StringUtil.isEmpty(outFileName)) { + outFileName = "index"; + + // Find a suitable extension + // TODO: Replace with MIME-type util with MIME/file ext mapping + String ext = conn.getContentType(); + + if (!StringUtil.isEmpty(ext)) { + int idx = ext.lastIndexOf('/'); + + if (idx >= 0) { + ext = ext.substring(idx + 1); + } + idx = ext.indexOf(';'); + if (idx >= 0) { + ext = ext.substring(0, idx); + } + outFileName += "." + ext; + } + } + } + int idx = outFileName.lastIndexOf('/'); + + if (idx >= 0) { + outFileName = outFileName.substring(idx + 1); + } + idx = outFileName.indexOf('?'); + if (idx >= 0) { + outFileName = outFileName.substring(0, idx); + } + } + File outFile = new File(outFileName); + + if (!outFile.createNewFile()) { + if (outFile.exists()) { + System.err.println("Cannot write to file " + outFile.getAbsolutePath() + ", file allready exists."); + } + else { + System.err.println("Cannot write to file " + outFile.getAbsolutePath() + ", check write permissions."); + } + System.exit(5); + } + os = new FileOutputStream(outFile); + } + else if (writeToStdOut) { + os = System.out; + } + else { + os = null; + } + + // Get data. + if ((writeToFile || writeToStdOut) && is != null) { + FileUtil.copy(is, os); + } + + /* + Hashtable postData = new Hashtable(); + postData.put("SearchText", "condition"); + + try { + InputStream in = getInputStreamHttpPost(pArgs[argIdx], postData, + props, true, 0); + out = new FileOutputStream(file); + FileUtil.copy(in, out); + } + catch (Exception e) { + System.err.println("Error: " + e); + e.printStackTrace(System.err); + continue; + } + */ + } + + /* + public static class Cookie { + String mName = null; + String mValue = null; + + public Cookie(String pName, String pValue) { + mName = pName; + mValue = pValue; + } + + public String toString() { + return mName + "=" + mValue; + } + */ + + /* + // Just a way to set cookies.. + if (pCookies != null) { + String cookieStr = ""; + for (int i = 0; i < pCookies.length; i++) + cookieStr += ((i == pCookies.length) ? pCookies[i].toString() + : pCookies[i].toString() + ";"); + + // System.out.println("Cookie: " + cookieStr); + + conn.setRequestProperty("Cookie", cookieStr); + } + */ + + /* + } + */ + + /** + * Test if the given URL is using HTTP protocol. + * + * @param pURL the url to condition + * @return true if the protocol is HTTP. + */ + public static boolean isHttpURL(String pURL) { + return ((pURL != null) && pURL.startsWith(HTTP)); + } + + /** + * Test if the given URL is using HTTP protocol. + * + * @param pURL the url to condition + * @return true if the protocol is HTTP. + */ + public static boolean isHttpURL(URL pURL) { + return ((pURL != null) && pURL.getProtocol().equals("http")); + } + + /** + * Gets the content from a given URL, and returns it as a byte array. + * Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * NOTE: If you supply a username and password for HTTP + * authentication, this method uses the java.net.Authenticator's static + * {@code setDefault()} method, that can only be set ONCE. This + * means that if the default Authenticator is allready set, this method + * will fail. + * It also means if any other piece of code tries to register a new default + * Authenticator within the current VM, it will fail. + * + * @param pURL A String containing the URL, on the form + * [http://][:@]servername[/file.ext] + * where everything in brackets are optional. + * @return a byte array with the URL contents. If an error occurs, the + * returned array may be zero-length, but not null. + * @throws MalformedURLException if the urlName parameter is not a valid + * URL. Note that the protocol cannot be anything but HTTP. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see java.net.Authenticator + * @see SimpleAuthenticator + */ + public static byte[] getBytesHttp(String pURL) throws IOException { + return getBytesHttp(pURL, 0); + } + + /** + * Gets the content from a given URL, and returns it as a byte array. + * + * @param pURL the URL to get. + * @return a byte array with the URL contents. If an error occurs, the + * returned array may be zero-length, but not null. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getBytesHttp(String) + */ + public static byte[] getBytesHttp(URL pURL) throws IOException { + return getBytesHttp(pURL, 0); + } + + /** + * Gets the InputStream from a given URL. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * NOTE: If you supply a username and password for HTTP + * authentication, this method uses the java.net.Authenticator's static + * {@code setDefault()} method, that can only be set ONCE. This + * means that if the default Authenticator is allready set, this method + * will fail. + * It also means if any other piece of code tries to register a new default + * Authenticator within the current VM, it will fail. + * + * @param pURL A String containing the URL, on the form + * [http://][:@]servername[/file.ext] + * where everything in brackets are optional. + * @return an input stream that reads from the connection created by the + * given URL. + * @throws MalformedURLException if the urlName parameter specifies an + * unknown protocol, or does not form a valid URL. + * Note that the protocol cannot be anything but HTTP. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see java.net.Authenticator + * @see SimpleAuthenticator + */ + public static InputStream getInputStreamHttp(String pURL) throws IOException { + return getInputStreamHttp(pURL, 0); + } + + /** + * Gets the InputStream from a given URL. + * + * @param pURL the URL to get. + * @return an input stream that reads from the connection created by the + * given URL. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(String) + */ + public static InputStream getInputStreamHttp(URL pURL) throws IOException { + return getInputStreamHttp(pURL, 0); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws MalformedURLException if the url parameter specifies an + * unknown protocol, or does not form a valid URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(URL,int) + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static InputStream getInputStreamHttp(String pURL, int pTimeout) throws IOException { + return getInputStreamHttp(pURL, null, true, pTimeout); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pProperties the request header properties. + * @param pFollowRedirects specifying wether redirects should be followed. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws MalformedURLException if the url parameter specifies an + * unknown protocol, or does not form a valid URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(URL,int) + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static InputStream getInputStreamHttp(final String pURL, final Properties pProperties, final boolean pFollowRedirects, final int pTimeout) + throws IOException { + + // Make sure we have properties + Properties properties = pProperties != null ? pProperties : new Properties(); + + //URL url = getURLAndRegisterPassword(pURL); + URL url = getURLAndSetAuthorization(pURL, properties); + + //unregisterPassword(url); + return getInputStreamHttp(url, properties, pFollowRedirects, pTimeout); + } + + /** + * Registers the password from the URL string, and returns the URL object. + * + * @param pURL the string representation of the URL, possibly including authorization part + * @param pProperties the + * @return the URL created from {@code pURL}. + * @throws java.net.MalformedURLException if there's a syntax error in {@code pURL} + */ + private static URL getURLAndSetAuthorization(final String pURL, final Properties pProperties) throws MalformedURLException { + String url = pURL; + // Split user/password away from url + String userPass = null; + String protocolPrefix = HTTP; + int httpIdx = url.indexOf(HTTPS); + + if (httpIdx >= 0) { + protocolPrefix = HTTPS; + url = url.substring(httpIdx + HTTPS.length()); + } + else { + httpIdx = url.indexOf(HTTP); + if (httpIdx >= 0) { + url = url.substring(httpIdx + HTTP.length()); + } + } + + // Get authorization part + int atIdx = url.indexOf("@"); + + if (atIdx >= 0) { + userPass = url.substring(0, atIdx); + url = url.substring(atIdx + 1); + } + + // Set authorization if user/password is present + if (userPass != null) { + // System.out.println("Setting password ("+ userPass + ")!"); + pProperties.setProperty("Authorization", "Basic " + BASE64.encode(userPass.getBytes())); + } + + // Return URL + return new URL(protocolPrefix + url); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see com.twelvemonkeys.net.HttpURLConnection + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see HttpURLConnection + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static InputStream getInputStreamHttp(URL pURL, int pTimeout) throws IOException { + return getInputStreamHttp(pURL, null, true, pTimeout); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pProperties the request header properties. + * @param pFollowRedirects specifying wether redirects should be followed. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(URL,int) + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static InputStream getInputStreamHttp(URL pURL, Properties pProperties, boolean pFollowRedirects, int pTimeout) + throws IOException { + + // Open the connection, and get the stream + HttpURLConnection conn = createHttpURLConnection(pURL, pProperties, pFollowRedirects, pTimeout); + + // HTTP GET method + conn.setRequestMethod(HTTP_GET); + + // This is where the connect happens + InputStream is = conn.getInputStream(); + + // We only accept the 200 OK message + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + throw new IOException("The request gave the response: " + conn.getResponseCode() + ": " + conn.getResponseMessage()); + } + return is; + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pPostData the post data. + * @param pProperties the request header properties. + * @param pFollowRedirects specifying wether redirects should be followed. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws MalformedURLException if the url parameter specifies an + * unknown protocol, or does not form a valid URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + */ + public static InputStream getInputStreamHttpPost(String pURL, Map pPostData, Properties pProperties, boolean pFollowRedirects, int pTimeout) + throws IOException { + + pProperties = pProperties != null ? pProperties : new Properties(); + + //URL url = getURLAndRegisterPassword(pURL); + URL url = getURLAndSetAuthorization(pURL, pProperties); + + //unregisterPassword(url); + return getInputStreamHttpPost(url, pPostData, pProperties, pFollowRedirects, pTimeout); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pPostData the post data. + * @param pProperties the request header properties. + * @param pFollowRedirects specifying wether redirects should be followed. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + */ + public static InputStream getInputStreamHttpPost(URL pURL, Map pPostData, Properties pProperties, boolean pFollowRedirects, int pTimeout) + throws IOException { + // Open the connection, and get the stream + HttpURLConnection conn = createHttpURLConnection(pURL, pProperties, pFollowRedirects, pTimeout); + + // HTTP POST method + conn.setRequestMethod(HTTP_POST); + + // Iterate over and create post data string + StringBuilder postStr = new StringBuilder(); + + if (pPostData != null) { + Iterator data = pPostData.entrySet().iterator(); + + while (data.hasNext()) { + Map.Entry entry = (Map.Entry) data.next(); + + // Properties key/values can be safely cast to strings + // Encode the string + postStr.append(URLEncoder.encode((String) entry.getKey(), "UTF-8")); + postStr.append('='); + postStr.append(URLEncoder.encode(entry.getValue().toString(), "UTF-8")); + + if (data.hasNext()) { + postStr.append('&'); + } + } + } + + // Set entity headers + String encoding = conn.getRequestProperty("Content-Encoding"); + if (StringUtil.isEmpty(encoding)) { + encoding = "UTF-8"; + } + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("Content-Length", String.valueOf(postStr.length())); + conn.setRequestProperty("Content-Encoding", encoding); + + // Get outputstream (this is where the connect actually happens) + OutputStream os = conn.getOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(os, encoding); + + // Write post data to the stream + writer.write(postStr.toString()); + writer.write("\r\n"); + writer.close(); // Does this close the underlying stream? + + // Get the inputstream + InputStream is = conn.getInputStream(); + + // We only accept the 200 OK message + // TODO: Accept all 200 messages, like ACCEPTED, CREATED or NO_CONTENT? + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + throw new IOException("The request gave the response: " + conn.getResponseCode() + ": " + conn.getResponseMessage()); + } + return is; + } + + /** + * Creates a HTTP connection to the given URL. + * + * @param pURL the URL to get. + * @param pProperties connection properties. + * @param pFollowRedirects specifies whether we should follow redirects. + * @param pTimeout the specified timeout, in milliseconds. + * @return a HttpURLConnection + * @throws UnknownHostException if the hostname in the URL cannot be found. + * @throws IOException if an I/O exception occurs. + */ + public static HttpURLConnection createHttpURLConnection(URL pURL, Properties pProperties, boolean pFollowRedirects, int pTimeout) + throws IOException { + + // Open the connection, and get the stream + HttpURLConnection conn; + + if (pTimeout > 0) { + // Supports timeout + conn = new com.twelvemonkeys.net.HttpURLConnection(pURL, pTimeout); + } + else { + // Faster, more compatible + conn = (HttpURLConnection) pURL.openConnection(); + } + + // Set user agent + if ((pProperties == null) || !pProperties.containsKey("User-Agent")) { + conn.setRequestProperty("User-Agent", + VERSION_ID + + " (" + System.getProperty("os.name") + "/" + System.getProperty("os.version") + "; " + + System.getProperty("os.arch") + "; " + + System.getProperty("java.vm.name") + "/" + System.getProperty("java.vm.version") + ")"); + } + + // Set request properties + if (pProperties != null) { + for (Map.Entry entry : pProperties.entrySet()) { + // Properties key/values can be safely cast to strings + conn.setRequestProperty((String) entry.getKey(), entry.getValue().toString()); + } + } + + try { + // Breaks with JRE1.2? + conn.setInstanceFollowRedirects(pFollowRedirects); + } + catch (LinkageError le) { + // This is the best we can do... + HttpURLConnection.setFollowRedirects(pFollowRedirects); + System.err.println("You are using an old Java Spec, consider upgrading."); + System.err.println("java.net.HttpURLConnection.setInstanceFollowRedirects(" + pFollowRedirects + ") failed."); + + //le.printStackTrace(System.err); + } + + conn.setDoInput(true); + conn.setDoOutput(true); + + //conn.setUseCaches(true); + return conn; + } + + /** + * This is a hack to get around the protected constructors in + * HttpURLConnection, should maybe consider registering and do things + * properly... + */ + + /* + private static class TimedHttpURLConnection + extends com.twelvemonkeys.net.HttpURLConnection { + TimedHttpURLConnection(URL pURL, int pTimeout) { + super(pURL, pTimeout); + } + } + */ + + /** + * Gets the content from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. + *
+ *
+ * + * @param pURL the URL to get. + * @param pTimeout the specified timeout, in milliseconds. + * @return a byte array that is read from the socket connection, created + * from the given URL. + * @throws MalformedURLException if the url parameter specifies an + * unknown protocol, or does not form a valid URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getBytesHttp(URL,int) + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static byte[] getBytesHttp(String pURL, int pTimeout) throws IOException { + // Get the input stream from the url + InputStream in = new BufferedInputStream(getInputStreamHttp(pURL, pTimeout), BUF_SIZE * 2); + + // Get all the bytes in loop + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int count; + byte[] buffer = new byte[BUF_SIZE]; + + try { + while ((count = in.read(buffer)) != -1) { + // NOTE: According to the J2SE API doc, read(byte[]) will read + // at least 1 byte, or return -1, if end-of-file is reached. + bytes.write(buffer, 0, count); + } + } + finally { + + // Close the buffer + in.close(); + } + return bytes.toByteArray(); + } + + /** + * Gets the content from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. + *
+ *
+ * + * @param pURL the URL to get. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(URL,int) + * @see com.twelvemonkeys.net.HttpURLConnection + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see HttpURLConnection + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static byte[] getBytesHttp(URL pURL, int pTimeout) throws IOException { + // Get the input stream from the url + InputStream in = new BufferedInputStream(getInputStreamHttp(pURL, pTimeout), BUF_SIZE * 2); + + // Get all the bytes in loop + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int count; + byte[] buffer = new byte[BUF_SIZE]; + + try { + while ((count = in.read(buffer)) != -1) { + // NOTE: According to the J2SE API doc, read(byte[]) will read + // at least 1 byte, or return -1, if end-of-file is reached. + bytes.write(buffer, 0, count); + } + } + finally { + + // Close the buffer + in.close(); + } + return bytes.toByteArray(); + } + + /** + * Unregisters the password asscociated with this URL + */ + + /* + private static void unregisterPassword(URL pURL) { + Authenticator auth = registerAuthenticator(); + if (auth != null && auth instanceof SimpleAuthenticator) + ((SimpleAuthenticator) auth) + .unregisterPasswordAuthentication(pURL); + } + */ + + /** + * Registers the password from the URL string, and returns the URL object. + */ + + /* + private static URL getURLAndRegisterPassword(String pURL) + throws MalformedURLException + { + // Split user/password away from url + String userPass = null; + String protocolPrefix = HTTP; + + int httpIdx = pURL.indexOf(HTTPS); + if (httpIdx >= 0) { + protocolPrefix = HTTPS; + pURL = pURL.substring(httpIdx + HTTPS.length()); + } + else { + httpIdx = pURL.indexOf(HTTP); + if (httpIdx >= 0) + pURL = pURL.substring(httpIdx + HTTP.length()); + } + + int atIdx = pURL.indexOf("@"); + if (atIdx >= 0) { + userPass = pURL.substring(0, atIdx); + pURL = pURL.substring(atIdx + 1); + } + + // Set URL + URL url = new URL(protocolPrefix + pURL); + + // Set Authenticator if user/password is present + if (userPass != null) { + // System.out.println("Setting password ("+ userPass + ")!"); + + int colIdx = userPass.indexOf(":"); + if (colIdx < 0) + throw new MalformedURLException("Error in username/password!"); + + String userName = userPass.substring(0, colIdx); + String passWord = userPass.substring(colIdx + 1); + + // Try to register the authenticator + // System.out.println("Trying to register authenticator!"); + Authenticator auth = registerAuthenticator(); + + // System.out.println("Got authenticator " + auth + "."); + + // Register our username/password with it + if (auth != null && auth instanceof SimpleAuthenticator) { + ((SimpleAuthenticator) auth) + .registerPasswordAuthentication(url, + new PasswordAuthentication(userName, + passWord.toCharArray())); + } + else { + // Not supported! + throw new RuntimeException("Could not register PasswordAuthentication"); + } + } + + return url; + } + */ + + /** + * Registers the Authenticator given in the system property + * {@code java.net.Authenticator}, or the default implementation + * ({@code com.twelvemonkeys.net.SimpleAuthenticator}). + *

+ * BUG: What if authenticator has allready been set outside this class? + * + * @return The Authenticator created and set as default, or null, if it + * was not set as the default. However, there is no (clean) way to + * be sure the authenticator was set (the SimpleAuthenticator uses + * a hack to get around this), so it might be possible that the + * returned authenticator was not set as default... + * @see Authenticator#setDefault(Authenticator) + * @see SimpleAuthenticator + */ + public synchronized static Authenticator registerAuthenticator() { + if (sAuthenticator != null) { + return sAuthenticator; + } + + // Get the system property + String authenticatorName = System.getProperty("java.net.Authenticator"); + + // Try to get the Authenticator from the system property + if (authenticatorName != null) { + try { + Class authenticatorClass = Class.forName(authenticatorName); + + sAuthenticator = (Authenticator) authenticatorClass.newInstance(); + } + catch (ClassNotFoundException cnfe) { + // We should maybe rethrow this? + } + catch (InstantiationException ie) { + // Ignore + } + catch (IllegalAccessException iae) { + // Ignore + } + } + + // Get the default authenticator + if (sAuthenticator == null) { + sAuthenticator = SimpleAuthenticator.getInstance(); + } + + // Register authenticator as default + Authenticator.setDefault(sAuthenticator); + return sAuthenticator; + } + + /** + * Creates the InetAddress object from the given URL. + * Equivalent to calling {@code InetAddress.getByName(URL.getHost())} + * except that it returns null, instead of throwing UnknownHostException. + * + * @param pURL the URL to look up. + * @return the createad InetAddress, or null if the host was unknown. + * @see java.net.InetAddress + * @see java.net.URL + */ + public static InetAddress createInetAddressFromURL(URL pURL) { + try { + return InetAddress.getByName(pURL.getHost()); + } + catch (UnknownHostException e) { + return null; + } + } + + /** + * Creates an URL from the given InetAddress object, using the given + * protocol. + * Equivalent to calling + * {@code new URL(protocol, InetAddress.getHostName(), "")} + * except that it returns null, instead of throwing MalformedURLException. + * + * @param pIP the IP address to look up + * @param pProtocol the protocol to use in the new URL + * @return the created URL or null, if the URL could not be created. + * @see java.net.URL + * @see java.net.InetAddress + */ + public static URL createURLFromInetAddress(InetAddress pIP, String pProtocol) { + try { + return new URL(pProtocol, pIP.getHostName(), ""); + } + catch (MalformedURLException e) { + return null; + } + } + + /** + * Creates an URL from the given InetAddress object, using HTTP protocol. + * Equivalent to calling + * {@code new URL("http", InetAddress.getHostName(), "")} + * except that it returns null, instead of throwing MalformedURLException. + * + * @param pIP the IP address to look up + * @return the created URL or null, if the URL could not be created. + * @see java.net.URL + * @see java.net.InetAddress + */ + public static URL createURLFromInetAddress(InetAddress pIP) { + return createURLFromInetAddress(pIP, HTTP); + } + + /* + * TODO: Benchmark! + */ + static byte[] getBytesHttpOld(String pURL) throws IOException { + // Get the input stream from the url + InputStream in = new BufferedInputStream(getInputStreamHttp(pURL), BUF_SIZE * 2); + + // Get all the bytes in loop + byte[] bytes = new byte[0]; + int count; + byte[] buffer = new byte[BUF_SIZE]; + + try { + while ((count = in.read(buffer)) != -1) { + + // NOTE: According to the J2SE API doc, read(byte[]) will read + // at least 1 byte, or return -1, if end-of-file is reached. + bytes = (byte[]) CollectionUtil.mergeArrays(bytes, 0, bytes.length, buffer, 0, count); + } + } + finally { + + // Close the buffer + in.close(); + } + return bytes; + } } \ No newline at end of file diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java similarity index 95% rename from common/common-io/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java index 3bbebcbb..81c68f66 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java @@ -1,45 +1,45 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * 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 "TwelveMonkeys" 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 com.twelvemonkeys.net; - -import java.net.*; - -/** - * Interface fro PasswordAuthenticators used by SimpleAuthenticator. - * - * @see SimpleAuthenticator - * @see java.net.Authenticator - * - * @author Harald Kuhr (haraldk@iconmedialab.no) - * - * @version 1.0 - */ -public interface PasswordAuthenticator { - public PasswordAuthentication requestPasswordAuthentication(InetAddress addr, int port, String protocol, String prompt, String scheme); -} +/* + * Copyright (c) 2008, Harald Kuhr + * 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 "TwelveMonkeys" 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 com.twelvemonkeys.net; + +import java.net.*; + +/** + * Interface fro PasswordAuthenticators used by SimpleAuthenticator. + * + * @see SimpleAuthenticator + * @see java.net.Authenticator + * + * @author Harald Kuhr + * + * @version 1.0 + */ +public interface PasswordAuthenticator { + public PasswordAuthentication requestPasswordAuthentication(InetAddress addr, int port, String protocol, String prompt, String scheme); +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java similarity index 97% rename from common/common-io/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java index d036fb2b..a7830581 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java @@ -1,270 +1,270 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * 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 "TwelveMonkeys" 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 com.twelvemonkeys.net; - -import com.twelvemonkeys.lang.Validate; - -import java.net.Authenticator; -import java.net.InetAddress; -import java.net.PasswordAuthentication; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; - -/** - * A simple Authenticator implementation. - * Singleton class, obtain reference through the static - * {@code getInstance} method. - *

- * After swearing, sweating, pulling my hair, banging my head repeatedly - * into the walls and reading the java.net.Authenticator API documentation - * once more, an idea came to my mind. This is the result. I hope you find it - * useful. -- Harald K. - * - * @author Harald Kuhr (haraldk@iconmedialab.no) - * @version 1.0 - * @see java.net.Authenticator - */ -public class SimpleAuthenticator extends Authenticator { - - /** The reference to the single instance of this class. */ - private static SimpleAuthenticator sInstance = null; - /** Keeps track of the state of this class. */ - private static boolean sInitialized = false; - - // These are used for the identification hack. - private final static String MAGIC = "magic"; - private final static int FOURTYTWO = 42; - - /** Basic authentication scheme. */ - public final static String BASIC = "Basic"; - - /** The hastable that keeps track of the PasswordAuthentications. */ - protected Map passwordAuthentications = null; - - /** The hastable that keeps track of the Authenticators. */ - protected Map authenticators = null; - - /** Creates a SimpleAuthenticator. */ - private SimpleAuthenticator() { - passwordAuthentications = new HashMap(); - authenticators = new HashMap(); - } - - /** - * Gets the SimpleAuthenticator instance and registers it through the - * Authenticator.setDefault(). If there is no current instance - * of the SimpleAuthenticator in the VM, one is created. This method will - * try to figure out if the setDefault() succeeded (a hack), and will - * return null if it was not able to register the instance as default. - * - * @return The single instance of this class, or null, if another - * Authenticator is allready registered as default. - */ - public static synchronized SimpleAuthenticator getInstance() { - if (!sInitialized) { - // Create an instance - sInstance = new SimpleAuthenticator(); - - // Try to set default (this may quietly fail...) - Authenticator.setDefault(sInstance); - - // A hack to figure out if we really did set the authenticator - PasswordAuthentication pa = Authenticator.requestPasswordAuthentication(null, FOURTYTWO, null, null, MAGIC); - - // If this test returns false, we didn't succeed, so we set the - // instance back to null. - if (pa == null || !MAGIC.equals(pa.getUserName()) || !("" + FOURTYTWO).equals(new String(pa.getPassword()))) { - sInstance = null; - } - - // Done - sInitialized = true; - } - - return sInstance; - } - - /** - * Gets the PasswordAuthentication for the request. Called when password - * authorization is needed. - * - * @return The PasswordAuthentication collected from the user, or null if - * none is provided. - */ - protected PasswordAuthentication getPasswordAuthentication() { - // Don't worry, this is just a hack to figure out if we were able - // to set this Authenticator through the setDefault method. - if (!sInitialized && MAGIC.equals(getRequestingScheme()) && getRequestingPort() == FOURTYTWO) { - return new PasswordAuthentication(MAGIC, ("" + FOURTYTWO).toCharArray()); - } - /* - System.err.println("getPasswordAuthentication"); - System.err.println(getRequestingSite()); - System.err.println(getRequestingPort()); - System.err.println(getRequestingProtocol()); - System.err.println(getRequestingPrompt()); - System.err.println(getRequestingScheme()); - */ - - // TODO: - // Look for a more specific PasswordAuthenticatior before using - // Default: - // - // if (...) - // return pa.requestPasswordAuthentication(getRequestingSite(), - // getRequestingPort(), - // getRequestingProtocol(), - // getRequestingPrompt(), - // getRequestingScheme()); - - return passwordAuthentications.get(new AuthKey(getRequestingSite(), - getRequestingPort(), - getRequestingProtocol(), - getRequestingPrompt(), - getRequestingScheme())); - } - - /** Registers a PasswordAuthentication with a given URL address. */ - public PasswordAuthentication registerPasswordAuthentication(URL pURL, PasswordAuthentication pPA) { - return registerPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), - pURL.getPort(), - pURL.getProtocol(), - null, // Prompt/Realm - BASIC, - pPA); - } - - /** Registers a PasswordAuthentication with a given net address. */ - public PasswordAuthentication registerPasswordAuthentication(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme, PasswordAuthentication pPA) { - /* - System.err.println("registerPasswordAuthentication"); - System.err.println(pAddress); - System.err.println(pPort); - System.err.println(pProtocol); - System.err.println(pPrompt); - System.err.println(pScheme); - */ - - return passwordAuthentications.put(new AuthKey(pAddress, pPort, pProtocol, pPrompt, pScheme), pPA); - } - - /** Unregisters a PasswordAuthentication with a given URL address. */ - public PasswordAuthentication unregisterPasswordAuthentication(URL pURL) { - return unregisterPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), pURL.getPort(), pURL.getProtocol(), null, BASIC); - } - - /** Unregisters a PasswordAuthentication with a given net address. */ - public PasswordAuthentication unregisterPasswordAuthentication(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme) { - return passwordAuthentications.remove(new AuthKey(pAddress, pPort, pProtocol, pPrompt, pScheme)); - } - - /** - * TODO: Registers a PasswordAuthenticator that can answer authentication - * requests. - * - * @see PasswordAuthenticator - */ - public void registerPasswordAuthenticator(PasswordAuthenticator pPA, AuthenticatorFilter pFilter) { - authenticators.put(pPA, pFilter); - } - - /** - * TODO: Unregisters a PasswordAuthenticator that can answer authentication - * requests. - * - * @see PasswordAuthenticator - */ - public void unregisterPasswordAuthenticator(PasswordAuthenticator pPA) { - authenticators.remove(pPA); - } -} - -/** - * Utility class, used for caching the PasswordAuthentication objects. - * Everything but address may be null - */ -class AuthKey { - - InetAddress address = null; - int port = -1; - String protocol = null; - String prompt = null; - String scheme = null; - - AuthKey(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme) { - Validate.notNull(pAddress, "address"); - - address = pAddress; - port = pPort; - protocol = pProtocol; - prompt = pPrompt; - scheme = pScheme; - - // System.out.println("Created: " + this); - } - - /** Creates a string representation of this object. */ - - public String toString() { - return "AuthKey[" + address + ":" + port + "/" + protocol + " \"" + prompt + "\" (" + scheme + ")]"; - } - - public boolean equals(Object pObj) { - return (pObj instanceof AuthKey && equals((AuthKey) pObj)); - } - - // Ahem.. Breaks the rule from Object.equals(Object): - // It is transitive: for any reference values x, y, and z, if x.equals(y) - // returns true and y.equals(z) returns true, then x.equals(z) - // should return true. - - public boolean equals(AuthKey pKey) { - // Maybe allow nulls, and still be equal? - return (address.equals(pKey.address) - && (port == -1 - || pKey.port == -1 - || port == pKey.port) - && (protocol == null - || pKey.protocol == null - || protocol.equals(pKey.protocol)) - && (prompt == null - || pKey.prompt == null - || prompt.equals(pKey.prompt)) - && (scheme == null - || pKey.scheme == null - || scheme.equalsIgnoreCase(pKey.scheme))); - } - - public int hashCode() { - // There won't be too many pr address, will it? ;-) - return address.hashCode(); - } -} - +/* + * Copyright (c) 2008, Harald Kuhr + * 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 "TwelveMonkeys" 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 com.twelvemonkeys.net; + +import com.twelvemonkeys.lang.Validate; + +import java.net.Authenticator; +import java.net.InetAddress; +import java.net.PasswordAuthentication; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * A simple Authenticator implementation. + * Singleton class, obtain reference through the static + * {@code getInstance} method. + *

+ * After swearing, sweating, pulling my hair, banging my head repeatedly + * into the walls and reading the java.net.Authenticator API documentation + * once more, an idea came to my mind. This is the result. I hope you find it + * useful. -- Harald K. + * + * @author Harald Kuhr + * @version 1.0 + * @see java.net.Authenticator + */ +public class SimpleAuthenticator extends Authenticator { + /** The reference to the single instance of this class. */ + private static SimpleAuthenticator sInstance = null; + /** Keeps track of the state of this class. */ + private static boolean sInitialized = false; + + // These are used for the identification hack. + private final static String MAGIC = "magic"; + private final static int FOURTYTWO = 42; + + /** Basic authentication scheme. */ + public final static String BASIC = "Basic"; + + /** The hastable that keeps track of the PasswordAuthentications. */ + protected Map passwordAuthentications = null; + + /** The hastable that keeps track of the Authenticators. */ + protected Map authenticators = null; + + /** Creates a SimpleAuthenticator. */ + private SimpleAuthenticator() { + passwordAuthentications = new HashMap(); + authenticators = new HashMap(); + } + + /** + * Gets the SimpleAuthenticator instance and registers it through the + * Authenticator.setDefault(). If there is no current instance + * of the SimpleAuthenticator in the VM, one is created. This method will + * try to figure out if the setDefault() succeeded (a hack), and will + * return null if it was not able to register the instance as default. + * + * @return The single instance of this class, or null, if another + * Authenticator is allready registered as default. + */ + public static synchronized SimpleAuthenticator getInstance() { + if (!sInitialized) { + // Create an instance + sInstance = new SimpleAuthenticator(); + + // Try to set default (this may quietly fail...) + Authenticator.setDefault(sInstance); + + // A hack to figure out if we really did set the authenticator + PasswordAuthentication pa = Authenticator.requestPasswordAuthentication(null, FOURTYTWO, null, null, MAGIC); + + // If this test returns false, we didn't succeed, so we set the + // instance back to null. + if (pa == null || !MAGIC.equals(pa.getUserName()) || !("" + FOURTYTWO).equals(new String(pa.getPassword()))) { + sInstance = null; + } + + // Done + sInitialized = true; + } + + return sInstance; + } + + /** + * Gets the PasswordAuthentication for the request. Called when password + * authorization is needed. + * + * @return The PasswordAuthentication collected from the user, or null if + * none is provided. + */ + protected PasswordAuthentication getPasswordAuthentication() { + // Don't worry, this is just a hack to figure out if we were able + // to set this Authenticator through the setDefault method. + if (!sInitialized && MAGIC.equals(getRequestingScheme()) && getRequestingPort() == FOURTYTWO) { + return new PasswordAuthentication(MAGIC, ("" + FOURTYTWO).toCharArray()); + } + /* + System.err.println("getPasswordAuthentication"); + System.err.println(getRequestingSite()); + System.err.println(getRequestingPort()); + System.err.println(getRequestingProtocol()); + System.err.println(getRequestingPrompt()); + System.err.println(getRequestingScheme()); + */ + + // TODO: + // Look for a more specific PasswordAuthenticatior before using + // Default: + // + // if (...) + // return pa.requestPasswordAuthentication(getRequestingSite(), + // getRequestingPort(), + // getRequestingProtocol(), + // getRequestingPrompt(), + // getRequestingScheme()); + + return passwordAuthentications.get(new AuthKey(getRequestingSite(), + getRequestingPort(), + getRequestingProtocol(), + getRequestingPrompt(), + getRequestingScheme())); + } + + /** Registers a PasswordAuthentication with a given URL address. */ + public PasswordAuthentication registerPasswordAuthentication(URL pURL, PasswordAuthentication pPA) { + return registerPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), + pURL.getPort(), + pURL.getProtocol(), + null, // Prompt/Realm + BASIC, + pPA); + } + + /** Registers a PasswordAuthentication with a given net address. */ + public PasswordAuthentication registerPasswordAuthentication(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme, PasswordAuthentication pPA) { + /* + System.err.println("registerPasswordAuthentication"); + System.err.println(pAddress); + System.err.println(pPort); + System.err.println(pProtocol); + System.err.println(pPrompt); + System.err.println(pScheme); + */ + + return passwordAuthentications.put(new AuthKey(pAddress, pPort, pProtocol, pPrompt, pScheme), pPA); + } + + /** Unregisters a PasswordAuthentication with a given URL address. */ + public PasswordAuthentication unregisterPasswordAuthentication(URL pURL) { + return unregisterPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), pURL.getPort(), pURL.getProtocol(), null, BASIC); + } + + /** Unregisters a PasswordAuthentication with a given net address. */ + public PasswordAuthentication unregisterPasswordAuthentication(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme) { + return passwordAuthentications.remove(new AuthKey(pAddress, pPort, pProtocol, pPrompt, pScheme)); + } + + /** + * TODO: Registers a PasswordAuthenticator that can answer authentication + * requests. + * + * @see PasswordAuthenticator + */ + public void registerPasswordAuthenticator(PasswordAuthenticator pPA, AuthenticatorFilter pFilter) { + authenticators.put(pPA, pFilter); + } + + /** + * TODO: Unregisters a PasswordAuthenticator that can answer authentication + * requests. + * + * @see PasswordAuthenticator + */ + public void unregisterPasswordAuthenticator(PasswordAuthenticator pPA) { + authenticators.remove(pPA); + } +} + +/** + * Utility class, used for caching the PasswordAuthentication objects. + * Everything but address may be null + */ +class AuthKey { + // TODO: Move this class to sandbox? + + InetAddress address = null; + int port = -1; + String protocol = null; + String prompt = null; + String scheme = null; + + AuthKey(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme) { + Validate.notNull(pAddress, "address"); + + address = pAddress; + port = pPort; + protocol = pProtocol; + prompt = pPrompt; + scheme = pScheme; + + // System.out.println("Created: " + this); + } + + /** Creates a string representation of this object. */ + + public String toString() { + return "AuthKey[" + address + ":" + port + "/" + protocol + " \"" + prompt + "\" (" + scheme + ")]"; + } + + public boolean equals(Object pObj) { + return (pObj instanceof AuthKey && equals((AuthKey) pObj)); + } + + // Ahem.. Breaks the rule from Object.equals(Object): + // It is transitive: for any reference values x, y, and z, if x.equals(y) + // returns true and y.equals(z) returns true, then x.equals(z) + // should return true. + + public boolean equals(AuthKey pKey) { + // Maybe allow nulls, and still be equal? + return (address.equals(pKey.address) + && (port == -1 + || pKey.port == -1 + || port == pKey.port) + && (protocol == null + || pKey.protocol == null + || protocol.equals(pKey.protocol)) + && (prompt == null + || pKey.prompt == null + || prompt.equals(pKey.prompt)) + && (scheme == null + || pKey.scheme == null + || scheme.equalsIgnoreCase(pKey.scheme))); + } + + public int hashCode() { + // There won't be too many pr address, will it? ;-) + return address.hashCode(); + } +} + diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java index ee567d8e..719bb378 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java @@ -29,7 +29,7 @@ package com.twelvemonkeys.servlet.cache; import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.net.NetUtil; +import com.twelvemonkeys.net.HTTPUtil; import com.twelvemonkeys.servlet.ServletResponseStreamDelegate; import javax.servlet.ServletOutputStream; @@ -212,7 +212,7 @@ class CacheResponseWrapper extends HttpServletResponseWrapper { if (Boolean.FALSE.equals(cacheable)) { super.setDateHeader(pName, pValue); } - cachedResponse.setHeader(pName, NetUtil.formatHTTPDate(pValue)); + cachedResponse.setHeader(pName, HTTPUtil.formatHTTPDate(pValue)); } public void addDateHeader(String pName, long pValue) { @@ -220,7 +220,7 @@ class CacheResponseWrapper extends HttpServletResponseWrapper { if (Boolean.FALSE.equals(cacheable)) { super.addDateHeader(pName, pValue); } - cachedResponse.addHeader(pName, NetUtil.formatHTTPDate(pValue)); + cachedResponse.addHeader(pName, HTTPUtil.formatHTTPDate(pValue)); } public void setHeader(String pName, String pValue) { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java index 1bae14e4..9d73e8b2 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java @@ -32,7 +32,7 @@ import com.twelvemonkeys.io.FileUtil; import com.twelvemonkeys.lang.StringUtil; import com.twelvemonkeys.lang.Validate; import com.twelvemonkeys.net.MIMEUtil; -import com.twelvemonkeys.net.NetUtil; +import com.twelvemonkeys.net.HTTPUtil; import com.twelvemonkeys.util.LRUHashMap; import com.twelvemonkeys.util.NullMap; @@ -972,7 +972,7 @@ public class HTTPCache { File cached = getCachedFile(pCacheURI, pRequest); if (cached != null && cached.exists()) { lastModified = cached.lastModified(); - //// System.out.println(" ## HTTPCache ## Last-Modified is " + NetUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); + //// System.out.println(" ## HTTPCache ## Last-Modified is " + HTTPUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); } } */ @@ -981,11 +981,11 @@ public class HTTPCache { int maxAge = getIntHeader(response, HEADER_CACHE_CONTROL, "max-age"); if (maxAge == -1) { expires = lastModified + defaultExpiryTime; - //// System.out.println(" ## HTTPCache ## Expires is " + NetUtil.formatHTTPDate(expires) + ", using lastModified + defaultExpiry"); + //// System.out.println(" ## HTTPCache ## Expires is " + HTTPUtil.formatHTTPDate(expires) + ", using lastModified + defaultExpiry"); } else { expires = lastModified + (maxAge * 1000L); // max-age is seconds - //// System.out.println(" ## HTTPCache ## Expires is " + NetUtil.formatHTTPDate(expires) + ", using lastModified + maxAge"); + //// System.out.println(" ## HTTPCache ## Expires is " + HTTPUtil.formatHTTPDate(expires) + ", using lastModified + maxAge"); } } /* @@ -997,7 +997,7 @@ public class HTTPCache { // Expired? if (expires < now) { // System.out.println(" ## HTTPCache ## Content is stale (content expired: " - // + NetUtil.formatHTTPDate(expires) + " before " + NetUtil.formatHTTPDate(now) + ")."); + // + HTTPUtil.formatHTTPDate(expires) + " before " + HTTPUtil.formatHTTPDate(now) + ")."); return true; } @@ -1008,7 +1008,7 @@ public class HTTPCache { File cached = getCachedFile(pCacheURI, pRequest); if (cached != null && cached.exists()) { lastModified = cached.lastModified(); - //// System.out.println(" ## HTTPCache ## Last-Modified is " + NetUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); + //// System.out.println(" ## HTTPCache ## Last-Modified is " + HTTPUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); } } */ @@ -1018,7 +1018,7 @@ public class HTTPCache { //noinspection RedundantIfStatement if (real != null && real.exists() && real.lastModified() > lastModified) { // System.out.println(" ## HTTPCache ## Content is stale (new content" - // + NetUtil.formatHTTPDate(lastModified) + " before " + NetUtil.formatHTTPDate(real.lastModified()) + ")."); + // + HTTPUtil.formatHTTPDate(lastModified) + " before " + HTTPUtil.formatHTTPDate(real.lastModified()) + ")."); return true; } @@ -1082,7 +1082,7 @@ public class HTTPCache { static long getDateHeader(final String pHeaderValue) { long date = -1L; if (pHeaderValue != null) { - date = NetUtil.parseHTTPDate(pHeaderValue); + date = HTTPUtil.parseHTTPDate(pHeaderValue); } return date; } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java index fd39747c..c73ebf99 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java @@ -29,7 +29,7 @@ package com.twelvemonkeys.servlet.cache; import com.twelvemonkeys.io.FastByteArrayOutputStream; -import com.twelvemonkeys.net.NetUtil; +import com.twelvemonkeys.net.HTTPUtil; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -53,7 +53,7 @@ class WritableCachedResponseImpl implements WritableCachedResponse { protected WritableCachedResponseImpl() { cachedResponse = new CachedResponseImpl(); // Hmmm.. - setHeader(HTTPCache.HEADER_CACHED_TIME, NetUtil.formatHTTPDate(System.currentTimeMillis())); + setHeader(HTTPCache.HEADER_CACHED_TIME, HTTPUtil.formatHTTPDate(System.currentTimeMillis())); } public CachedResponse getCachedResponse() { diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java index b5b69f45..dbe759aa 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java @@ -1,6 +1,6 @@ package com.twelvemonkeys.servlet.cache; -import com.twelvemonkeys.net.NetUtil; +import com.twelvemonkeys.net.HTTPUtil; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; @@ -644,7 +644,7 @@ public class HTTPCacheTestCase { CacheResponse res = (CacheResponse) invocation.getArguments()[1]; res.setStatus(HttpServletResponse.SC_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Date", HTTPUtil.formatHTTPDate(System.currentTimeMillis())); res.setHeader("Cache-Control", "public"); res.addHeader("X-Custom", "FOO"); res.addHeader("X-Custom", "BAR"); @@ -1126,7 +1126,7 @@ public class HTTPCacheTestCase { CacheResponse res = (CacheResponse) invocation.getArguments()[1]; res.setStatus(status); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Date", HTTPUtil.formatHTTPDate(System.currentTimeMillis())); for (Map.Entry> header : headers.entrySet()) { for (String value : header.getValue()) { From 2f69847b23ea2e3f3947f76de3b9d89dc1177b74 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sun, 8 Sep 2013 14:16:31 +0200 Subject: [PATCH 06/98] Moved old obsolete stuff to sandbox. --- .../servlet/cache/SerlvetCacheResponseWrapper.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java index e88123f4..f71f3d8b 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java @@ -29,7 +29,7 @@ package com.twelvemonkeys.servlet.cache; import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.net.NetUtil; +import com.twelvemonkeys.net.HTTPUtil; import com.twelvemonkeys.servlet.ServletResponseStreamDelegate; import javax.servlet.ServletOutputStream; @@ -224,7 +224,7 @@ class SerlvetCacheResponseWrapper extends HttpServletResponseWrapper { if (Boolean.FALSE.equals(cacheable)) { super.setDateHeader(pName, pValue); } - cacheResponse.setHeader(pName, NetUtil.formatHTTPDate(pValue)); + cacheResponse.setHeader(pName, HTTPUtil.formatHTTPDate(pValue)); } public void addDateHeader(String pName, long pValue) { @@ -232,7 +232,7 @@ class SerlvetCacheResponseWrapper extends HttpServletResponseWrapper { if (Boolean.FALSE.equals(cacheable)) { super.addDateHeader(pName, pValue); } - cacheResponse.addHeader(pName, NetUtil.formatHTTPDate(pValue)); + cacheResponse.addHeader(pName, HTTPUtil.formatHTTPDate(pValue)); } public void setHeader(String pName, String pValue) { From 13c8cd7f931c30c92b9ea295071fe64221a4d9f4 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sun, 8 Sep 2013 14:18:38 +0200 Subject: [PATCH 07/98] POM changes in sandbox. --- sandbox/pom.xml | 8 +++++++- sandbox/sandbox-imageio/pom.xml | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/sandbox/pom.xml b/sandbox/pom.xml index 20decb8b..2a022f46 100644 --- a/sandbox/pom.xml +++ b/sandbox/pom.xml @@ -79,6 +79,12 @@ ${project.version} provided + + com.twelvemonkeys.sandbox + sandbox-common + ${project.version} + compile + com.twelvemonkeys.common @@ -120,7 +126,7 @@ org.apache.maven.plugins maven-jar-plugin - 2.2 + 2.4 diff --git a/sandbox/sandbox-imageio/pom.xml b/sandbox/sandbox-imageio/pom.xml index c574bd69..4974cd80 100644 --- a/sandbox/sandbox-imageio/pom.xml +++ b/sandbox/sandbox-imageio/pom.xml @@ -40,7 +40,7 @@ jar TwelveMonkeys :: Sandbox :: ImageIO - The TwelveMonkeys ImageIO Sandbox. Experimental stuff. Old retired stuff. + The TwelveMonkeys ImageIO Sandbox. New experimental stuff. Old retired stuff. @@ -62,6 +62,12 @@ compile + + com.twelvemonkeys.sandbox + sandbox-common + compile + + com.twelvemonkeys.common common-io From 55bd82491f7a966b7ac8e5b80a81bf1dc21ea075 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sun, 8 Sep 2013 14:20:13 +0200 Subject: [PATCH 08/98] Moved old obsolete stuff to sandbox. --- .../common-io/src/main/java/com/twelvemonkeys/net/HTTPUtil.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/HTTPUtil.java b/common/common-io/src/main/java/com/twelvemonkeys/net/HTTPUtil.java index ba670646..399adf29 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/HTTPUtil.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/net/HTTPUtil.java @@ -98,6 +98,8 @@ public class HTTPUtil { } } + private HTTPUtil() {} + /** * Formats the time to a HTTP date, using the RFC 1123 format, as described * in Date: Sun, 8 Sep 2013 14:21:51 +0200 Subject: [PATCH 09/98] TMC-IO: Fixed typos, no functional changes --- .../src/main/java/com/twelvemonkeys/io/FileUtil.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/FileUtil.java b/common/common-io/src/main/java/com/twelvemonkeys/io/FileUtil.java index f538d742..8cad4665 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/FileUtil.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/FileUtil.java @@ -79,7 +79,7 @@ public final class FileUtil { /* * Method main for test only. - */ + * public static void main0(String[] pArgs) { if (pArgs.length != 2) { System.out.println("usage: java Copy in out"); @@ -94,6 +94,7 @@ public final class FileUtil { System.out.println(e.getMessage()); } } + //*/ // Avoid instances/constructor showing up in API doc private FileUtil() {} @@ -204,7 +205,7 @@ public final class FileUtil { close(out); } - return true; // If we got here, everything's probably okay.. ;-) + return true; // If we got here, everything is probably okay.. ;-) } /** @@ -581,7 +582,7 @@ public final class FileUtil { * @throws IOException if an i/o error occurs during read. */ public static byte[] read(InputStream pInput) throws IOException { - // Create bytearray + // Create byte array ByteArrayOutputStream bytes = new FastByteArrayOutputStream(BUF_SIZE); // Copy from stream to byte array From 10b95b225f280a15c99285aa20fa7a03cfd9f9ae Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sun, 8 Sep 2013 14:23:13 +0200 Subject: [PATCH 10/98] TMI-CORE: Added comments/fixed typos no functional changes. --- .../main/java/com/twelvemonkeys/imageio/ImageReaderBase.java | 2 +- .../main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java index 4a0d7243..ca0129b2 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java @@ -235,7 +235,7 @@ public abstract class ImageReaderBase extends ImageReader { // If param is non-null, use it if (param != null) { - // Try to get the explicit destinaton image + // Try to get the explicit destination image BufferedImage dest = param.getDestination(); if (dest != null) { diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java index 767e6b8e..b5968606 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/ColorSpaces.java @@ -209,6 +209,7 @@ public final class ColorSpaces { // being 1 (01000000) - "Media Relative Colormetric" in the offending profiles, // and 0 (00000000) - "Perceptual" in the good profiles // (that is 1 single bit of difference right there.. ;-) + // See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7064516 // This is particularly annoying, as the byte copying isn't really necessary, // except the getRenderingIntent method is package protected in java.awt.color From dc63fac8ef092966b407f151db572ae6f2dbc7f9 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sun, 8 Sep 2013 14:25:13 +0200 Subject: [PATCH 11/98] Updated JZ2012 demo/example code --- .../twelvemonkeys/image/AbstractFilter.java | 143 +++++++++++ .../twelvemonkeys/image/InstaCRTFilter.java | 182 ++++++++++++++ .../twelvemonkeys/image/InstaLomoFilter.java | 201 +++++++++++++++ .../twelvemonkeys/image/InstaSepiaFilter.java | 150 ++++++++++++ .../com/twelvemonkeys/image/NoiseFilter.java | 228 ++++++++++++++++++ 5 files changed, 904 insertions(+) create mode 100644 sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/AbstractFilter.java create mode 100644 sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaCRTFilter.java create mode 100644 sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaLomoFilter.java create mode 100644 sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaSepiaFilter.java create mode 100644 sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/NoiseFilter.java diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/AbstractFilter.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/AbstractFilter.java new file mode 100644 index 00000000..064fae79 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/AbstractFilter.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * 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 "TwelveMonkeys" 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 com.twelvemonkeys.image; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ColorModel; +import java.io.File; +import java.io.IOException; + +/** + * AbstractFilter + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: AbstractFilter.java,v 1.0 18.06.12 16:55 haraldk Exp$ + */ +public abstract class AbstractFilter implements BufferedImageOp { + public abstract BufferedImage filter(BufferedImage src, BufferedImage dest); + + public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) { + throw new UnsupportedOperationException("Method createCompatibleDestImage not implemented"); // TODO: Implement + } + + public Rectangle2D getBounds2D(BufferedImage src) { + return new Rectangle2D.Double(0, 0, src.getWidth(), src.getHeight()); + } + + public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { + if (dstPt == null) { + dstPt = new Point2D.Double(); + } + + dstPt.setLocation(srcPt); + + return dstPt; + } + + public RenderingHints getRenderingHints() { + return null; + } + + protected static void exercise(final String[] args, final BufferedImageOp filter, final Color background) throws IOException { + boolean original = false; + + for (String arg : args) { + if (arg.startsWith("-")) { + if (arg.equals("-o") || arg.equals("--original")) { + original = true; + } + + continue; + } + + final File file = new File(arg); + BufferedImage image = ImageIO.read(file); + + if (image.getWidth() > 640) { + image = new ResampleOp(640, Math.round(image.getHeight() * (640f / image.getWidth())), null).filter(image, null); + } + + if (!original) { + filter.filter(image, image); + } + + final Color bg = original ? Color.BLACK : background; + final BufferedImage img = image; + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + JFrame frame = new JFrame(filter.getClass().getSimpleName().replace("Filter", "") + "Test: " + file.getName()); + frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(final WindowEvent e) { + Window[] windows = Window.getWindows(); + if (windows == null || windows.length == 0) { + System.exit(0); + } + } + }); + frame.getRootPane().getActionMap().put("window-close", new AbstractAction() { + public void actionPerformed(ActionEvent e) { + Window window = SwingUtilities.getWindowAncestor((Component) e.getSource()); + window.setVisible(false); + window.dispose(); + } + }); + frame.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_W, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "window-close"); + + JLabel label = new JLabel(new BufferedImageIcon(img)); + if (bg != null) { + label.setOpaque(true); + label.setBackground(bg); + } + label.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + JScrollPane scrollPane = new JScrollPane(label); + scrollPane.setBorder(BorderFactory.createEmptyBorder()); + frame.add(scrollPane); + + frame.pack(); + frame.setLocationByPlatform(true); + frame.setVisible(true); + } + }); + } + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaCRTFilter.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaCRTFilter.java new file mode 100644 index 00000000..1929d7fe --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaCRTFilter.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * 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 "TwelveMonkeys" 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 com.twelvemonkeys.image; + +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.awt.image.ColorConvertOp; +import java.awt.image.RescaleOp; +import java.io.IOException; +import java.util.Random; + +/** + * InstaCRTFilter + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: InstaCRTFilter.java,v 1.0 15.06.12 13:24 haraldk Exp$ + */ +public class InstaCRTFilter extends AbstractFilter { + + // NOTE: This is a PoC, and not good code... + public BufferedImage filter(BufferedImage src, BufferedImage dest) { + if (dest == null) { + dest = createCompatibleDestImage(src, null); + } + + // Make grayscale + BufferedImage image = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), getRenderingHints()).filter(src, null); + + // Make image faded/too bright + image = new RescaleOp(1.2f, 120f, getRenderingHints()).filter(image, image); + + // Blur + image = ImageUtil.blur(image, 2.5f); + + Graphics2D g = dest.createGraphics(); + try { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + g.drawImage(image, 0, 0, null); + + // Rotate it slightly for a more analogue feeling + double angle = .0055; + g.rotate(angle); + + // Apply fake green-ish h-sync line at random position + Random random = new Random(); + int lineStart = random.nextInt(image.getHeight() - 80); + int lineHeight = random.nextInt(10) + 20; + + g.setComposite(AlphaComposite.SrcOver.derive(.3f)); + g.setPaint(new LinearGradientPaint( + 0, lineStart, 0, lineStart + lineHeight, + new float[] {0, .3f, .9f, 1}, + new Color[] {new Color(0, true), new Color(0x90AF66), new Color(0x99606F33, true), new Color(0, true)} + )); + g.fillRect(0, lineStart, image.getWidth(), lineHeight); + + // Apply fake large dot-pitch (black lines w/transparency) + g.setComposite(AlphaComposite.SrcOver.derive(.55f)); + g.setColor(Color.BLACK); + + for (int y = 0; y < image.getHeight(); y += 3) { + g.setStroke(new BasicStroke(random.nextFloat() / 3 + .8f)); + g.drawLine(0, y, image.getWidth(), y); + } + + // Vignette/border + g.setComposite(AlphaComposite.SrcOver.derive(.75f)); + int focus = Math.min(image.getWidth() / 8, image.getHeight() / 8); + g.setPaint(new RadialGradientPaint( + new Point(image.getWidth() / 2, image.getHeight() / 2), + Math.max(image.getWidth(), image.getHeight()) / 1.6f, + new Point(focus, focus), + new float[] {0, .3f, .9f, 1f}, + new Color[] {new Color(0x99FFFFFF, true), new Color(0x00FFFFFF, true), new Color(0x0, true), Color.BLACK}, + MultipleGradientPaint.CycleMethod.NO_CYCLE + )); + g.fillRect(-2, -2, image.getWidth() + 4, image.getHeight() + 4); + + g.rotate(-angle); + + g.setComposite(AlphaComposite.SrcOver.derive(.35f)); + g.setPaint(new RadialGradientPaint( + new Point(image.getWidth() / 2, image.getHeight() / 2), + Math.max(image.getWidth(), image.getHeight()) / 1.65f, + new Point(image.getWidth() / 2, image.getHeight() / 2), + new float[] {0, .85f, 1f}, + new Color[] {new Color(0x0, true), new Color(0x0, true), Color.BLACK}, + MultipleGradientPaint.CycleMethod.NO_CYCLE + )); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + + // Highlight + g.setComposite(AlphaComposite.SrcOver.derive(.55f)); + g.setPaint(new RadialGradientPaint( + new Point(image.getWidth(), image.getHeight()), + Math.max(image.getWidth(), image.getHeight()) * 1.1f, + new Point(image.getWidth() / 2, image.getHeight() / 2), + new float[] {0, .75f, 1f}, + new Color[] {new Color(0x00FFFFFF, true), new Color(0x00FFFFFF, true), Color.WHITE}, + MultipleGradientPaint.CycleMethod.NO_CYCLE + )); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + } + finally { + g.dispose(); + } + + // Round corners + BufferedImage foo = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = foo.createGraphics(); + try { + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + graphics.setColor(Color.WHITE); + double angle = -0.04; + g.rotate(angle); + graphics.fillRoundRect(1, 1, image.getWidth() - 2, image.getHeight() - 2, 20, 20); + } + finally { + graphics.dispose(); + } + + foo = ImageUtil.blur(foo, 4.5f); + + // Compose image into rounded corners + graphics = foo.createGraphics(); + try { + graphics.setComposite(AlphaComposite.SrcIn); + graphics.drawImage(dest, 0, 0, null); + } + finally { + graphics.dispose(); + } + + // Draw it all back to dest + g = dest.createGraphics(); + try { + g.setComposite(AlphaComposite.SrcOver); + g.setColor(Color.BLACK); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + g.drawImage(foo, 0, 0, null); + } + finally { + g.dispose(); + } + + return dest; + } + + public static void main(String[] args) throws IOException { + exercise(args, new InstaCRTFilter(), Color.BLACK); + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaLomoFilter.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaLomoFilter.java new file mode 100644 index 00000000..6d277717 --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaLomoFilter.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * 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 "TwelveMonkeys" 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 com.twelvemonkeys.image; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.RescaleOp; +import java.io.IOException; +import java.util.Random; + +/** + * InstaLomoFilter + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: InstaLomoFilter.java,v 1.0 15.06.12 13:24 haraldk Exp$ + */ +public class InstaLomoFilter extends AbstractFilter { + final private Random random = new Random(); + + // NOTE: This is a PoC, and not good code... + public BufferedImage filter(BufferedImage src, BufferedImage dest) { + if (dest == null) { + dest = createCompatibleDestImage(src, null); + } + + // Make image faded/washed out/red-ish + // DARK WARM + float[] scales = new float[] { 2.2f, 2.0f, 1.55f}; + float[] offsets = new float[] {-20.0f, -90.0f, -110.0f}; + + // BRIGHT NATURAL +// float[] scales = new float[] { 1.1f, .9f, .7f}; +// float[] offsets = new float[] {20, 30, 80}; + + // Faded, old-style +// float[] scales = new float[] { 1.1f, .7f, .3f}; +// float[] offsets = new float[] {20, 30, 80}; + +// float[] scales = new float[] { 1.2f, .4f, .4f}; +// float[] offsets = new float[] {0, 120, 120}; + + // BRIGHT WARM +// float[] scales = new float[] {1.1f, .8f, 1.6f}; +// float[] offsets = new float[] {60, 70, -80}; + BufferedImage image = new RescaleOp(scales, offsets, getRenderingHints()).filter(src, null); + + // Blur + image = ImageUtil.blur(image, 2.5f); + + Graphics2D g = dest.createGraphics(); + try { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + g.drawImage(image, 0, 0, null); + + // Rotate it slightly for a more analogue feeling + double angle = .0055; + g.rotate(angle); + + // Scratches + g.setComposite(AlphaComposite.SrcOver.derive(.025f)); + for (int i = 0; i < 100; i++) { + g.setColor(random.nextBoolean() ? Color.WHITE : Color.BLACK); + g.setStroke(new BasicStroke(random.nextFloat() * 2f)); + int x = random.nextInt(image.getWidth()); + + int off = random.nextInt(100); + for (int j = random.nextInt(3); j > 0; j--) { + g.drawLine(x + j, 0, x + off - 50 + j, image.getHeight()); + } + } + + // Vignette/border + g.setComposite(AlphaComposite.SrcOver.derive(.75f)); + int focus = Math.min(image.getWidth() / 8, image.getHeight() / 8); + g.setPaint(new RadialGradientPaint( + new Point(image.getWidth() / 2, image.getHeight() / 2), + Math.max(image.getWidth(), image.getHeight()) / 1.6f, + new Point(focus, focus), + new float[] {0, .3f, .9f, 1f}, + new Color[] {new Color(0x99FFFFFF, true), new Color(0x00FFFFFF, true), new Color(0x0, true), Color.BLACK}, + MultipleGradientPaint.CycleMethod.NO_CYCLE + )); + g.fillRect(-2, -2, image.getWidth() + 4, image.getHeight() + 4); + + g.rotate(-angle); + + g.setComposite(AlphaComposite.SrcOver.derive(.35f)); + g.setPaint(new RadialGradientPaint( + new Point(image.getWidth() / 2, image.getHeight() / 2), + Math.max(image.getWidth(), image.getHeight()) / 1.65f, + new Point(image.getWidth() / 2, image.getHeight() / 2), + new float[] {0, .85f, 1f}, + new Color[] {new Color(0x0, true), new Color(0x0, true), Color.BLACK}, + MultipleGradientPaint.CycleMethod.NO_CYCLE + )); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + + // Highlight + g.setComposite(AlphaComposite.SrcOver.derive(.35f)); + g.setPaint(new RadialGradientPaint( + new Point(image.getWidth(), image.getHeight()), + Math.max(image.getWidth(), image.getHeight()) * 1.1f, + new Point(image.getWidth() / 2, image.getHeight() / 2), + new float[] {0, .75f, 1f}, + new Color[] {new Color(0x00FFFFFF, true), new Color(0x00FFFFFF, true), Color.PINK}, + MultipleGradientPaint.CycleMethod.NO_CYCLE + )); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + } + finally { + g.dispose(); + } + + // Noise + NoiseFilter noise = new NoiseFilter(); + noise.setAmount(10); + noise.setDensity(2); + dest = noise.filter(dest, dest); + + // Round corners + BufferedImage foo = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = foo.createGraphics(); + try { + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + graphics.setColor(Color.WHITE); + double angle = (random.nextDouble() * .01) - .005; + graphics.rotate(angle); + graphics.fillRoundRect(4, 4, image.getWidth() - 8, image.getHeight() - 8, 20, 20); + } + finally { + graphics.dispose(); + } + + noise.setAmount(20); + noise.setDensity(1); + noise.setMonochrome(true); + foo = noise.filter(foo, foo); + + foo = ImageUtil.blur(foo, 4.5f); + + // Compose image into rounded corners + graphics = foo.createGraphics(); + try { + graphics.setComposite(AlphaComposite.SrcIn); + graphics.drawImage(dest, 0, 0, null); + } + finally { + graphics.dispose(); + } + + // Draw it all back to dest + g = dest.createGraphics(); + try { + if (dest.getTransparency() != Transparency.OPAQUE) { + g.setComposite(AlphaComposite.Clear); + } + g.setColor(Color.WHITE); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + g.setComposite(AlphaComposite.SrcOver); + g.drawImage(foo, 0, 0, null); + } + finally { + g.dispose(); + } + + return dest; + } + + public static void main(String[] args) throws IOException { + exercise(args, new InstaLomoFilter(), Color.WHITE); + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaSepiaFilter.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaSepiaFilter.java new file mode 100644 index 00000000..b82c6b9c --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/InstaSepiaFilter.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * 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 "TwelveMonkeys" 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 com.twelvemonkeys.image; + +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.image.*; +import java.io.IOException; +import java.util.Random; + +/** + * InstaLomoFilter + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: InstaLomoFilter.java,v 1.0 15.06.12 13:24 haraldk Exp$ + */ +public class InstaSepiaFilter extends AbstractFilter { + final private Random random = new Random(); + + // NOTE: This is a PoC, and not good code... + @Override + public BufferedImage filter(BufferedImage src, BufferedImage dest) { + if (dest == null) { + dest = createCompatibleDestImage(src, null); + } + + BufferedImage image = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), getRenderingHints()).filter(src, dest); + + Graphics2D g2d = dest.createGraphics(); + try { + g2d.drawImage(image, 0, 0, null); + } + finally { + g2d.dispose(); + } + + // Blur + image = ImageUtil.blur(image, 2.5f); + + Graphics2D g = dest.createGraphics(); + try { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + g.drawImage(image, 0, 0, null); + + // Rotate it slightly for a more analogue feeling + double angle = -.0055; + g.rotate(angle); + + // Vignette/border + g.setComposite(AlphaComposite.SrcOver.derive(.35f)); + g.setPaint(new RadialGradientPaint( + new Point(image.getWidth() / 2, image.getHeight() / 2), + Math.max(image.getWidth(), image.getHeight()) / 1.65f, + new Point(image.getWidth() / 2, image.getHeight() / 2), + new float[] {0, .85f, 1f}, + new Color[] {new Color(0x0, true), new Color(0x0, true), Color.BLACK}, + MultipleGradientPaint.CycleMethod.NO_CYCLE + )); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + + } + finally { + g.dispose(); + } + + // Round corners + BufferedImage foo = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = foo.createGraphics(); + try { + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + graphics.setColor(Color.WHITE); + double angle = (random.nextDouble() * .01) - .005; + graphics.rotate(angle); + graphics.fillRoundRect(4, 4, image.getWidth() - 8, image.getHeight() - 8, 20, 20); + } + finally { + graphics.dispose(); + } + + // Noise + NoiseFilter noise = new NoiseFilter(); + noise.setAmount(20); + noise.setDensity(1); + noise.setMonochrome(true); + foo = noise.filter(foo, foo); + + foo = ImageUtil.blur(foo, 4.5f); + + // Compose image into rounded corners + graphics = foo.createGraphics(); + try { + graphics.setComposite(AlphaComposite.SrcIn); + graphics.drawImage(dest, 0, 0, null); + } + finally { + graphics.dispose(); + } + + float[] scales = new float[] {1, 1, 1, 1}; + float[] offsets = new float[] {80, 40, 0, 0}; + foo = new RescaleOp(scales, offsets, getRenderingHints()).filter(foo, foo); + + // Draw it all back to dest + g = dest.createGraphics(); + try { + g.setComposite(AlphaComposite.SrcOver); + g.setColor(Color.WHITE); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + g.drawImage(foo, 0, 0, null); + } + finally { + g.dispose(); + } + + return dest; + } + + public static void main(String[] args) throws IOException { + exercise(args, new InstaSepiaFilter(), null); + } +} diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/NoiseFilter.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/NoiseFilter.java new file mode 100644 index 00000000..73c8d50b --- /dev/null +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/NoiseFilter.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2012, Harald Kuhr + * 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 "TwelveMonkeys" 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. + */ +/* +Copyright 2006 Jerry Huxtable + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.twelvemonkeys.image; + +import java.awt.image.BufferedImage; +import java.awt.image.WritableRaster; +import java.util.Random; + +/** + * NoiseFilter + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: NoiseFilter.java,v 1.0 15.06.12 22:59 haraldk Exp$ + */ +public class NoiseFilter extends AbstractFilter { + + /** + * Gaussian distribution for the noise. + */ + public final static int GAUSSIAN = 0; + + /** + * Uniform distribution for the noise. + */ + public final static int UNIFORM = 1; + + private int amount = 25; + private int distribution = UNIFORM; + private boolean monochrome = false; + private float density = 1; + private Random randomNumbers = new Random(); + + public NoiseFilter() { + } + + /** + * Set the amount of effect. + * + * @param amount the amount + * @min-value 0 + * @max-value 1 + * @see #getAmount + */ + public void setAmount(int amount) { + this.amount = amount; + } + + /** + * Get the amount of noise. + * + * @return the amount + * @see #setAmount + */ + public int getAmount() { + return amount; + } + + /** + * Set the distribution of the noise. + * + * @param distribution the distribution + * @see #getDistribution + */ + public void setDistribution(int distribution) { + this.distribution = distribution; + } + + /** + * Get the distribution of the noise. + * + * @return the distribution + * @see #setDistribution + */ + public int getDistribution() { + return distribution; + } + + /** + * Set whether to use monochrome noise. + * + * @param monochrome true for monochrome noise + * @see #getMonochrome + */ + public void setMonochrome(boolean monochrome) { + this.monochrome = monochrome; + } + + /** + * Get whether to use monochrome noise. + * + * @return true for monochrome noise + * @see #setMonochrome + */ + public boolean getMonochrome() { + return monochrome; + } + + /** + * Set the density of the noise. + * + * @param density the density + * @see #getDensity + */ + public void setDensity(float density) { + this.density = density; + } + + /** + * Get the density of the noise. + * + * @return the density + * @see #setDensity + */ + public float getDensity() { + return density; + } + + private int random() { + return (int) (((distribution == GAUSSIAN ? randomNumbers.nextGaussian() : 2 * randomNumbers.nextFloat() - 1)) * amount); + } + + private static int clamp(int x) { + if (x < 0) { + return 0; + } + else if (x > 0xff) { + return 0xff; + } + return x; + } + + public int filterRGB(int x, int y, int rgb) { + if (randomNumbers.nextFloat() <= density) { + int a = rgb & 0xff000000; + int r = (rgb >> 16) & 0xff; + int g = (rgb >> 8) & 0xff; + int b = rgb & 0xff; + + if (monochrome) { + int n = random(); + r = clamp(r + n); + g = clamp(g + n); + b = clamp(b + n); + } + else { + r = clamp(r + random()); + g = clamp(g + random()); + b = clamp(b + random()); + } + return a | (r << 16) | (g << 8) | b; + } + return rgb; + } + + public BufferedImage filter(BufferedImage src, BufferedImage dst) { + int width = src.getWidth(); + int height = src.getHeight(); + int type = src.getType(); + WritableRaster srcRaster = src.getRaster(); + + if (dst == null) { + dst = createCompatibleDestImage(src, null); + } + WritableRaster dstRaster = dst.getRaster(); + + int[] inPixels = new int[width]; + for (int y = 0; y < height; y++) { + // We try to avoid calling getRGB on images as it causes them to become unmanaged, causing horrible performance problems. + if (type == BufferedImage.TYPE_INT_ARGB) { + srcRaster.getDataElements(0, y, width, 1, inPixels); + for (int x = 0; x < width; x++) { + inPixels[x] = filterRGB(x, y, inPixels[x]); + } + dstRaster.setDataElements(0, y, width, 1, inPixels); + } + else { + src.getRGB(0, y, width, 1, inPixels, 0, width); + for (int x = 0; x < width; x++) { + inPixels[x] = filterRGB(x, y, inPixels[x]); + } + dst.setRGB(0, y, width, 1, inPixels, 0, width); + } + } + + return dst; + } +} From 5531c863cffdb7cd282702fd5ade75207bf1a748 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sun, 8 Sep 2013 14:27:08 +0200 Subject: [PATCH 12/98] TMI-JPEG: Fixed typos in exception messages. --- .../imageio/plugins/jpeg/EXIFThumbnailReader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java index 1eaba702..a54645ae 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java @@ -202,7 +202,7 @@ final class EXIFThumbnailReader extends ThumbnailReader { Entry width = ifd.getEntryById(TIFF.TAG_IMAGE_WIDTH); if (width == null) { - throw new IIOException("Missing dimensions for unknown EXIF thumbnail"); + throw new IIOException("Missing dimensions for uncompressed EXIF thumbnail"); } return ((Number) width.getValue()).intValue(); @@ -221,7 +221,7 @@ final class EXIFThumbnailReader extends ThumbnailReader { Entry height = ifd.getEntryById(TIFF.TAG_IMAGE_HEIGHT); if (height == null) { - throw new IIOException("Missing dimensions for unknown EXIF thumbnail"); + throw new IIOException("Missing dimensions for uncompressed EXIF thumbnail"); } return ((Number) height.getValue()).intValue(); From cdc832623a2bcbcd7d3a47b602bd6b055173afe0 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sun, 8 Sep 2013 14:33:40 +0200 Subject: [PATCH 13/98] TMI-METADATA: Minor clean-up, preparing for read/write of metadata. Now uses proper constants for TIFF types. --- .../imageio/metadata/AbstractEntry.java | 6 ++-- .../imageio/metadata/exif/EXIFEntry.java | 2 ++ .../imageio/metadata/exif/EXIFReader.java | 35 ++++++++++--------- .../imageio/metadata/exif/TIFF.java | 17 +++++++++ .../imageio/metadata/jpeg/JPEGSegment.java | 2 ++ 5 files changed, 42 insertions(+), 20 deletions(-) diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java index 9b5170ab..99f6b524 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java @@ -44,7 +44,7 @@ import java.util.Arrays; public abstract class AbstractEntry implements Entry { private final Object identifier; - private final Object value; // TODO: Might need to be mutable.. + private final Object value; // Entries are immutable, directories can be mutated protected AbstractEntry(final Object identifier, final Object value) { Validate.notNull(identifier, "identifier"); @@ -181,10 +181,10 @@ public abstract class AbstractEntry implements Entry { @Override public String toString() { String name = getFieldName(); - String nameStr = name != null ? "/" + name + "" : ""; + String nameStr = name != null ? String.format("/%s", name) : ""; String type = getTypeName(); - String typeStr = type != null ? " (" + type + ")" : ""; + String typeStr = type != null ? String.format(" (%s)", type) : ""; return String.format("%s%s: %s%s", getNativeIdentifier(), nameStr, getValueAsString(), typeStr); } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java index cf10da19..50dbe332 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java @@ -46,6 +46,8 @@ final class EXIFEntry extends AbstractEntry { if (type < 1 || type > TIFF.TYPE_NAMES.length) { throw new IllegalArgumentException(String.format("Illegal EXIF type: %s", type)); } + + // TODO: Validate that type is applicable to value? this.type = type; } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java index 8497da35..02a8fab6 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java @@ -110,7 +110,6 @@ public final class EXIFReader extends MetadataReader { // Read linked IFDs if (nextOffset != 0) { - // TODO: This is probably not okay anymore.. Replace recursion with while loop AbstractCompoundDirectory next = (AbstractCompoundDirectory) readDirectory(pInput, nextOffset); for (int i = 0; i < next.directoryCount(); i++) { ifds.add((IFD) next.getDirectory(i)); @@ -298,7 +297,7 @@ public final class EXIFReader extends MetadataReader { long pos = pInput.getStreamPosition(); switch (pType) { - case 2: // ASCII + case TIFF.TYPE_ASCII: // TODO: This might be UTF-8 or ISO-8859-x, even though spec says NULL-terminated 7 bit ASCII // TODO: Fail if unknown chars, try parsing with ISO-8859-1 or file.encoding if (pCount == 0) { @@ -308,17 +307,17 @@ public final class EXIFReader extends MetadataReader { pInput.readFully(ascii); int len = ascii[ascii.length - 1] == 0 ? ascii.length - 1 : ascii.length; return StringUtil.decode(ascii, 0, len, "UTF-8"); // UTF-8 is ASCII compatible - case 1: // BYTE + case TIFF.TYPE_BYTE: if (pCount == 1) { return pInput.readUnsignedByte(); } // else fall through - case 6: // SBYTE + case TIFF.TYPE_SBYTE: if (pCount == 1) { return pInput.readByte(); } // else fall through - case 7: // UNDEFINED + case TIFF.TYPE_UNDEFINED: byte[] bytes = new byte[pCount]; pInput.readFully(bytes); @@ -326,11 +325,11 @@ public final class EXIFReader extends MetadataReader { // binary data and we want to keep that as a byte array for clients to parse futher return bytes; - case 3: // SHORT + case TIFF.TYPE_SHORT: if (pCount == 1) { return pInput.readUnsignedShort(); } - case 8: // SSHORT + case TIFF.TYPE_SSHORT: if (pCount == 1) { return pInput.readShort(); } @@ -338,21 +337,22 @@ public final class EXIFReader extends MetadataReader { short[] shorts = new short[pCount]; pInput.readFully(shorts, 0, shorts.length); - if (pType == 3) { + if (pType == TIFF.TYPE_SHORT) { int[] ints = new int[pCount]; for (int i = 0; i < pCount; i++) { ints[i] = shorts[i] & 0xffff; } + return ints; } return shorts; - case 13: // IFD - case 4: // LONG + case TIFF.TYPE_IFD: + case TIFF.TYPE_LONG: if (pCount == 1) { return pInput.readUnsignedInt(); } - case 9: // SLONG + case TIFF.TYPE_SLONG: if (pCount == 1) { return pInput.readInt(); } @@ -360,16 +360,17 @@ public final class EXIFReader extends MetadataReader { int[] ints = new int[pCount]; pInput.readFully(ints, 0, ints.length); - if (pType == 4 || pType == 13) { + if (pType == TIFF.TYPE_LONG || pType == TIFF.TYPE_IFD) { long[] longs = new long[pCount]; for (int i = 0; i < pCount; i++) { longs[i] = ints[i] & 0xffffffffL; } + return longs; } return ints; - case 11: // FLOAT + case TIFF.TYPE_FLOAT: if (pCount == 1) { return pInput.readFloat(); } @@ -377,7 +378,7 @@ public final class EXIFReader extends MetadataReader { float[] floats = new float[pCount]; pInput.readFully(floats, 0, floats.length); return floats; - case 12: // DOUBLE + case TIFF.TYPE_DOUBLE: if (pCount == 1) { return pInput.readDouble(); } @@ -386,7 +387,7 @@ public final class EXIFReader extends MetadataReader { pInput.readFully(doubles, 0, doubles.length); return doubles; - case 5: // RATIONAL + case TIFF.TYPE_RATIONAL: if (pCount == 1) { return createSafeRational(pInput.readUnsignedInt(), pInput.readUnsignedInt()); } @@ -397,7 +398,7 @@ public final class EXIFReader extends MetadataReader { } return rationals; - case 10: // SRATIONAL + case TIFF.TYPE_SRATIONAL: if (pCount == 1) { return createSafeRational(pInput.readInt(), pInput.readInt()); } @@ -445,7 +446,7 @@ public final class EXIFReader extends MetadataReader { return new Rational(numerator, denominator); } - private int getValueLength(final int pType, final int pCount) { + static int getValueLength(final int pType, final int pCount) { if (pType > 0 && pType <= TIFF.TYPE_LENGTHS.length) { return TIFF.TYPE_LENGTHS[pType - 1] * pCount; } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java index 574bfb4d..375fec62 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java @@ -37,8 +37,25 @@ package com.twelvemonkeys.imageio.metadata.exif; */ @SuppressWarnings("UnusedDeclaration") public interface TIFF { + short BYTE_ORDER_MARK_BIG_ENDIAN = ('M' << 8) | 'M'; + short BYTE_ORDER_MARK_LITTLE_ENDIAN = ('I' << 8) | 'I'; + int TIFF_MAGIC = 42; + short TYPE_BYTE = 1; + short TYPE_ASCII = 2; + short TYPE_SHORT = 3; + short TYPE_LONG = 4; + short TYPE_RATIONAL = 5; + + short TYPE_SBYTE = 6; + short TYPE_UNDEFINED = 7; + short TYPE_SSHORT = 8; + short TYPE_SLONG = 9; + short TYPE_SRATIONAL = 10; + short TYPE_FLOAT = 11; + short TYPE_DOUBLE = 12; + short TYPE_IFD = 13; /* 1 = BYTE 8-bit unsigned integer. 2 = ASCII 8-bit byte that contains a 7-bit ASCII code; the last byte diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java index 2d667b6c..4de9bd34 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java @@ -77,6 +77,8 @@ public final class JPEGSegment implements Serializable { return marker >= 0xFFE0 && marker <= 0xFFEF; } + // TODO: Consider returning an ImageInputStream and use ByteArrayImageInputStream directly, for less wrapping and better performance + // TODO: BUT: Must find a way to skip padding in/after segment identifier (eg: Exif has null-term + null-pad, ICC_PROFILE has only null-term). Is data always word-aligned? public InputStream data() { return data != null ? new ByteArrayInputStream(data, offset(), length()) : null; } From 2116feb49fe47610a56bb0aca833d91830562f50 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sun, 8 Sep 2013 14:38:49 +0200 Subject: [PATCH 14/98] Experimental implementation of Base64 encoded DataURL string. Probably not a good idea.. ;-) --- .../Base64DataURLImageInputStreamSpi.java | 65 +++++++++++++++++++ .../Base64DataURLImageInputStreamTest.java | 44 +++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 sandbox/sandbox-imageio/src/main/java/com/twelvemonkeys/imageio/stream/Base64DataURLImageInputStreamSpi.java create mode 100644 sandbox/sandbox-imageio/src/test/java/com/twelvemonkeys/imageio/stream/Base64DataURLImageInputStreamTest.java diff --git a/sandbox/sandbox-imageio/src/main/java/com/twelvemonkeys/imageio/stream/Base64DataURLImageInputStreamSpi.java b/sandbox/sandbox-imageio/src/main/java/com/twelvemonkeys/imageio/stream/Base64DataURLImageInputStreamSpi.java new file mode 100644 index 00000000..eab14369 --- /dev/null +++ b/sandbox/sandbox-imageio/src/main/java/com/twelvemonkeys/imageio/stream/Base64DataURLImageInputStreamSpi.java @@ -0,0 +1,65 @@ +package com.twelvemonkeys.imageio.stream; + +import com.twelvemonkeys.io.StringInputStream; +import com.twelvemonkeys.io.enc.Base64Decoder; +import com.twelvemonkeys.io.enc.DecoderStream; + +import javax.imageio.spi.ImageInputStreamSpi; +import javax.imageio.stream.FileCacheImageInputStream; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.MemoryCacheImageInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Locale; + +/** + * Base64DataURLImageInputStreamSpi + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: Base64DataURLImageInputStreamSpi.java,v 1.0 03.09.13 09:35 haraldk Exp$ + */ +public class Base64DataURLImageInputStreamSpi extends ImageInputStreamSpi { + // This is generally a bad idea, because: + // - It is bound to String.class, and not all strings are base64 encoded data URLs. + // - It's better to just create a decoder stream from the base64 stream, and use what's already in ImageIO.... + + public Base64DataURLImageInputStreamSpi() { + super("TwelveMonkeys", "0.1-BETA", String.class); + } + + @Override + public ImageInputStream createInputStreamInstance(final Object input, final boolean useCache, final File cacheDir) throws IOException { + String string = (String) input; + + InputStream stream = createStreamFromBase64(string); + + return useCache && cacheDir != null ? new FileCacheImageInputStream(stream, cacheDir) : new MemoryCacheImageInputStream(stream); + } + + private InputStream createStreamFromBase64(String string) { + if (!string.startsWith("data:")) { + throw new IllegalArgumentException(String.format("Not a data URL: %s", string)); + } + + int index = string.indexOf(';'); + if (index < 0 || !string.regionMatches(index + 1, "base64,", 0, "base64,".length())) { + throw new IllegalArgumentException(String.format("Not base64 encoded: %s", string)); + } + + int offset = index + "base64,".length() + 1; + return new DecoderStream(new StringInputStream(string.substring(offset), Charset.forName("UTF-8")), new Base64Decoder()); + } + + @Override + public boolean canUseCacheFile() { + return true; + } + + @Override + public String getDescription(Locale locale) { + return "Service provider that instantiates a FileCacheImageInputStream or MemoryCacheImageInputStream from a Base64 encoded data string"; + } +} diff --git a/sandbox/sandbox-imageio/src/test/java/com/twelvemonkeys/imageio/stream/Base64DataURLImageInputStreamTest.java b/sandbox/sandbox-imageio/src/test/java/com/twelvemonkeys/imageio/stream/Base64DataURLImageInputStreamTest.java new file mode 100644 index 00000000..60a8e18f --- /dev/null +++ b/sandbox/sandbox-imageio/src/test/java/com/twelvemonkeys/imageio/stream/Base64DataURLImageInputStreamTest.java @@ -0,0 +1,44 @@ +package com.twelvemonkeys.imageio.stream; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import javax.imageio.ImageIO; +import javax.imageio.spi.IIORegistry; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; + +import static org.junit.Assert.*; + +/** + * Base64DataURLImageInputStreamTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: Base64DataURLImageInputStreamTest.java,v 1.0 04.09.13 13:47 haraldk Exp$ + */ +public class Base64DataURLImageInputStreamTest { + static final String DATA = "data:image/png;base64,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"; + static final Base64DataURLImageInputStreamSpi provider = new Base64DataURLImageInputStreamSpi(); + + @Before + public void init() { + IIORegistry.getDefaultInstance().registerServiceProvider(provider); + } + + @After + public void destroy() { + IIORegistry.getDefaultInstance().deregisterServiceProvider(provider); + } + + @Test + public void testRead() throws IOException, InvocationTargetException, InterruptedException { + BufferedImage image = ImageIO.read(ImageIO.createImageInputStream(DATA)); + + assertNotNull(image); + assertEquals(56, image.getWidth()); + assertEquals(34, image.getHeight()); + } +} From 47425e2ca0f70469bce9a3dbd216da390b210cea Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sun, 8 Sep 2013 14:41:06 +0200 Subject: [PATCH 15/98] Rewritten to use SunWritableRaster if available, with graceful fallback. Better tiling in test app. --- .../image/MappedBufferImage.java | 46 ++++++--- .../image/MappedImageFactory.java | 99 +++++++++++++++++-- 2 files changed, 123 insertions(+), 22 deletions(-) diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java index 8d69ffce..2954364c 100644 --- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedBufferImage.java @@ -61,6 +61,7 @@ import java.util.concurrent.*; public class MappedBufferImage { private static int threads = Runtime.getRuntime().availableProcessors(); private static ExecutorService executorService = Executors.newFixedThreadPool(threads * 4); + private static ExecutorService executorService2 = Executors.newFixedThreadPool(2); public static void main(String[] args) throws IOException { int argIndex = 0; @@ -553,15 +554,15 @@ public class MappedBufferImage { } } - public void drawTo(Graphics2D g) { + public boolean drawTo(Graphics2D g) { BufferedImage img = data.get(); if (img != null) { g.drawImage(img, x, y, null); + return true; } -// g.setPaint(Color.GREEN); -// g.drawString(String.format("[%d, %d]", x, y), x + 20, y + 20); + return false; } public int getX() { @@ -622,6 +623,7 @@ public class MappedBufferImage { } // TODO: Consider a fixed size (mem) LRUCache instead + // TODO: Better yet, re-use tiles Map tiles = createTileCache(); private void repaintImage(final Rectangle rect, final Graphics2D g2) { @@ -634,6 +636,15 @@ public class MappedBufferImage { // Paint tiles of the image, to preserve memory final int tileSize = 200; + // Calculate relative to image(0,0), rather than rect(x, y) + int xOff = rect.x % tileSize; + int yOff = rect.y % tileSize; + + rect.x -= xOff; + rect.y -= yOff; + rect.width += xOff; + rect.height += yOff; + int tilesW = 1 + rect.width / tileSize; int tilesH = 1 + rect.height / tileSize; @@ -658,10 +669,10 @@ public class MappedBufferImage { // TODO: Could we use ImageProducer/ImageConsumer/ImageObserver interface?? // Destination (display) coordinates - int dstX = (int) Math.round(x * zoom); - int dstY = (int) Math.round(y * zoom); - int dstW = (int) Math.round(w * zoom); - int dstH = (int) Math.round(h * zoom); + int dstX = (int) Math.floor(x * zoom); + int dstY = (int) Math.floor(y * zoom); + int dstW = (int) Math.ceil(w * zoom); + int dstH = (int) Math.ceil(h * zoom); if (dstW == 0 || dstH == 0) { continue; @@ -678,8 +689,8 @@ public class MappedBufferImage { // final int tileSrcH = Math.min(tileSize, image.getHeight() - tileSrcY); // Destination (display) coordinates - int tileDstX = (int) Math.round(tileSrcX * zoom); - int tileDstY = (int) Math.round(tileSrcY * zoom); + int tileDstX = (int) Math.floor(tileSrcX * zoom); + int tileDstY = (int) Math.floor(tileSrcY * zoom); // final int tileDstW = (int) Math.round(tileSrcW * zoom); // final int tileDstH = (int) Math.round(tileSrcH * zoom); @@ -699,9 +710,7 @@ public class MappedBufferImage { Tile tile = tiles.get(point); if (tile != null) { - Reference img = tile.data; - if (img != null) { - tile.drawTo(g2); + if (tile.drawTo(g2)) { continue; } else { @@ -713,9 +722,8 @@ public class MappedBufferImage { // Dispatch to off-thread worker final Map localTiles = tiles; - executorService.submit(new Runnable() { + executorService2.submit(new Runnable() { public void run() { - // TODO: Fix rounding issues... Problem is that sometimes the srcW/srcH is 1 pixel off filling the tile... int tileSrcX = (int) Math.round(point.x / zoom); int tileSrcY = (int) Math.round(point.y / zoom); int tileSrcW = Math.min(tileSize, image.getWidth() - tileSrcX); @@ -735,8 +743,14 @@ public class MappedBufferImage { } // Test against current view rect, to avoid computing tiles that will be thrown away immediately - // TODO: EDT safe? - if (!getVisibleRect().intersects(new Rectangle(point.x, point.y, tileDstW, tileDstH))) { + final Rectangle visibleRect = new Rectangle(); + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + visibleRect.setBounds(getVisibleRect()); + } + }); + + if (!visibleRect.intersects(new Rectangle(point.x, point.y, tileDstW, tileDstH))) { return; } diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedImageFactory.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedImageFactory.java index 3347dd38..fb72da96 100644 --- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedImageFactory.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/image/MappedImageFactory.java @@ -30,11 +30,12 @@ package com.twelvemonkeys.image; import javax.imageio.ImageTypeSpecifier; import java.awt.*; -import java.awt.image.BufferedImage; -import java.awt.image.ColorModel; -import java.awt.image.DataBuffer; -import java.awt.image.SampleModel; +import java.awt.image.*; import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.lang.reflect.UndeclaredThrowableException; /** * A factory for creating {@link BufferedImage}s backed by memory mapped files. @@ -50,6 +51,9 @@ public final class MappedImageFactory { // TODO: Create a way to do ColorConvertOp (or other color space conversion) on these images. // - Current implementation of CCOp delegates to internal sun.awt classes that assumes java.awt.DataBufferByte for type byte buffers :-/ + private static final boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.image.mapped.debug")); + static final RasterFactory RASTER_FACTORY = createRasterFactory(); + private MappedImageFactory() {} public static BufferedImage createCompatibleMappedImage(int width, int height, int type) throws IOException { @@ -58,7 +62,8 @@ public final class MappedImageFactory { } public static BufferedImage createCompatibleMappedImage(int width, int height, GraphicsConfiguration configuration, int transparency) throws IOException { - // TODO: Should we also use the sample model? +// BufferedImage temp = configuration.createCompatibleImage(1, 1, transparency); +// return createCompatibleMappedImage(width, height, temp.getSampleModel().createCompatibleSampleModel(width, height), temp.getColorModel()); return createCompatibleMappedImage(width, height, configuration.getColorModel(transparency)); } @@ -73,6 +78,88 @@ public final class MappedImageFactory { static BufferedImage createCompatibleMappedImage(int width, int height, SampleModel sm, ColorModel cm) throws IOException { DataBuffer buffer = MappedFileBuffer.create(sm.getTransferType(), width * height * sm.getNumDataElements(), 1); - return new BufferedImage(cm, new GenericWritableRaster(sm, buffer, new Point()), cm.isAlphaPremultiplied(), null); + return new BufferedImage(cm, RASTER_FACTORY.createRaster(sm, buffer, new Point()), cm.isAlphaPremultiplied(), null); + } + + private static RasterFactory createRasterFactory() { + try { + // Try to instantiate, will throw LinkageError if it fails + return new SunRasterFactory(); + } + catch (LinkageError e) { + if (DEBUG) { + e.printStackTrace(); + } + + System.err.println("Could not instantiate SunWritableRaster, falling back to GenericWritableRaster."); + } + + // Fall back + return new GenericRasterFactory(); + } + + static interface RasterFactory { + WritableRaster createRaster(SampleModel model, DataBuffer buffer, Point origin); + } + + /** + * Generic implementation that should work for any JRE, and creates a custom subclass of {@link WritableRaster}. + */ + static final class GenericRasterFactory implements RasterFactory { + public WritableRaster createRaster(final SampleModel model, final DataBuffer buffer, final Point origin) { + return new GenericWritableRaster(model, buffer, origin); + } + } + + /** + * Sun/Oracle JRE-specific implementation that creates {@code sun.awt.image.SunWritableRaster}. + * Callers must catch {@link LinkageError}. + */ + static final class SunRasterFactory implements RasterFactory { + final private Constructor factoryMethod = getFactoryMethod(); + + @SuppressWarnings("unchecked") + private static Constructor getFactoryMethod() { + try { + Class cls = Class.forName("sun.awt.image.SunWritableRaster"); + + if (Modifier.isAbstract(cls.getModifiers())) { + throw new IncompatibleClassChangeError("sun.awt.image.SunWritableRaster has become abstract and can't be instantiated"); + } + + return (Constructor) cls.getConstructor(SampleModel.class, DataBuffer.class, Point.class); + } + catch (ClassNotFoundException e) { + throw new NoClassDefFoundError(e.getMessage()); + } + catch (NoSuchMethodException e) { + throw new NoSuchMethodError(e.getMessage()); + } + } + + public WritableRaster createRaster(final SampleModel model, final DataBuffer buffer, final Point origin) { + try { + return factoryMethod.newInstance(model, buffer, origin); + } + catch (InstantiationException e) { + throw new Error("Could not create SunWritableRaster: ", e); // Should never happen, as we test for abstract class + } + catch (IllegalAccessException e) { + throw new Error("Could not create SunWritableRaster: ", e); // Should never happen, only public constructors are reflected + } + catch (InvocationTargetException e) { + // Unwrap to allow normal exception flow + Throwable cause = e.getCause(); + + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + else if (cause instanceof Error) { + throw (Error) cause; + } + + throw new UndeclaredThrowableException(cause); + } + } } } From 0ff99afe6d6f02da323a3ff8672061eb6d82f290 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sun, 8 Sep 2013 14:43:05 +0200 Subject: [PATCH 16/98] TMI-JPEG: Now does a better effort to gloss over metadata issues in underlying stream. --- .../imageio/plugins/jpeg/JPEGImageReader.java | 69 +++++++++++++++---- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 81b7269e..d56df77e 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -41,13 +41,16 @@ import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment; import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; import com.twelvemonkeys.imageio.util.ProgressListenerBase; import com.twelvemonkeys.lang.Validate; +import org.w3c.dom.Node; import javax.imageio.*; import javax.imageio.event.IIOReadUpdateListener; import javax.imageio.event.IIOReadWarningListener; import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.MemoryCacheImageInputStream; import java.awt.*; import java.awt.color.ColorSpace; import java.awt.color.ICC_ColorSpace; @@ -89,6 +92,8 @@ public class JPEGImageReader extends ImageReaderBase { // TODO: Fix the (stream) metadata inconsistency issues. // - Sun JPEGMetadata class does not (and can not be made to) support CMYK data.. We need to create all new metadata classes.. :-/ + // TODO: Allow automatic rotation based on EXIF rotation field? + private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug")); /** Segment identifiers for the JPEG segments we care about reading. */ @@ -280,7 +285,19 @@ public class JPEGImageReader extends ImageReaderBase { assertInput(); checkBounds(imageIndex); - // TODO: This test is not good enough for JDK7, which seems to have fixed some of the issues. +// CompoundDirectory exif = getExif(); +// if (exif != null) { +// System.err.println("exif: " + exif); +// System.err.println("Orientation: " + exif.getEntryById(TIFF.TAG_ORIENTATION)); +// Entry exifIFDEntry = exif.getEntryById(TIFF.TAG_EXIF_IFD); +// +// if (exifIFDEntry != null) { +// Directory exifIFD = (Directory) exifIFDEntry.getValue(); +// System.err.println("PixelXDimension: " + exifIFD.getEntryById(EXIF.TAG_PIXEL_X_DIMENSION)); +// System.err.println("PixelYDimension: " + exifIFD.getEntryById(EXIF.TAG_PIXEL_Y_DIMENSION)); +// } +// } + // NOTE: We rely on the fact that unsupported images has no valid types. This is kind of hacky. // Might want to look into the metadata, to see if there's a better way to identify these. boolean unsupported = !delegate.getImageTypes(imageIndex).hasNext(); @@ -288,7 +305,6 @@ public class JPEGImageReader extends ImageReaderBase { ICC_Profile profile = getEmbeddedICCProfile(false); AdobeDCTSegment adobeDCT = getAdobeDCT(); - // TODO: Probably something bogus here, as ICC profile isn't applied if reading through the delegate any more... // We need to apply ICC profile unless the profile is sRGB/default gray (whatever that is) // - or only filter out the bad ICC profiles in the JPEGSegmentImageInputStream. if (delegate.canReadRaster() && ( @@ -307,7 +323,7 @@ public class JPEGImageReader extends ImageReaderBase { if (DEBUG) { System.out.println("Reading using delegate"); } - + return delegate.read(imageIndex, param); } @@ -442,6 +458,8 @@ public class JPEGImageReader extends ImageReaderBase { // Apply further color conversion for explicit color space, or just copy the pixels into place if (convert != null) { convert.filter(src, dest); +// WritableRaster filtered = convert.filter(src, null); +// new AffineTransformOp(AffineTransform.getRotateInstance(2 * Math.PI, filtered.getWidth() / 2.0, filtered.getHeight() / 2.0), null).filter(filtered, dest); } else { dest.setRect(0, 0, src); @@ -728,12 +746,12 @@ public class JPEGImageReader extends ImageReaderBase { private JFIFSegment getJFIF() throws IOException{ List jfif = getAppSegments(JPEG.APP0, "JFIF"); - + if (!jfif.isEmpty()) { JPEGSegment segment = jfif.get(0); return JFIFSegment.read(segment.data()); } - + return null; } @@ -748,6 +766,27 @@ public class JPEGImageReader extends ImageReaderBase { return null; } + private CompoundDirectory getExif() throws IOException { + List exifSegments = getAppSegments(JPEG.APP1, "Exif"); + + if (!exifSegments.isEmpty()) { + JPEGSegment exif = exifSegments.get(0); + InputStream data = exif.data(); + + if (data.read() == -1) { // Read pad + processWarningOccurred("Exif chunk has no data."); + } + else { + ImageInputStream stream = ImageIO.createImageInputStream(data); + return (CompoundDirectory) new EXIFReader().read(stream); + + // TODO: Directory offset of thumbnail is wrong/relative to container stream, causing trouble for the EXIFReader... + } + } + + return null; + } + // TODO: Util method? static byte[] readFully(DataInput stream, int len) throws IOException { if (len == 0) { @@ -911,7 +950,7 @@ public class JPEGImageReader extends ImageReaderBase { processWarningOccurred("Exif chunk has no data."); } else { - ImageInputStream stream = ImageIO.createImageInputStream(data); + ImageInputStream stream = new MemoryCacheImageInputStream(data); CompoundDirectory exifMetadata = (CompoundDirectory) new EXIFReader().read(stream); if (exifMetadata.directoryCount() == 2) { @@ -965,16 +1004,15 @@ public class JPEGImageReader extends ImageReaderBase { return thumbnails.get(thumbnailIndex).read(); } - // Metadata @Override public IIOMetadata getImageMetadata(int imageIndex) throws IOException { - // TODO: Nice try, but no cigar.. getAsTree does not return a "live" view, so any modifications are thrown away IIOMetadata metadata = delegate.getImageMetadata(imageIndex); -// IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName()); -// Node jpegVariety = tree.getElementsByTagName("JPEGvariety").item(0); + String format = metadata.getNativeMetadataFormatName(); + IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(format); + Node jpegVariety = tree.getElementsByTagName("JPEGvariety").item(0); // TODO: Allow EXIF (as app1EXIF) in the JPEGvariety (sic) node. // As EXIF is (a subset of) TIFF, (and the EXIF data is a valid TIFF stream) probably use something like: @@ -996,12 +1034,17 @@ public class JPEGImageReader extends ImageReaderBase { the version to the method/constructor used to obtain an IIOMetadata object.) */ -// IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC"); -// app2ICC.setUserObject(getEmbeddedICCProfile()); -// jpegVariety.getFirstChild().appendChild(app2ICC); + IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC"); + app2ICC.setUserObject(getEmbeddedICCProfile(true)); + Node jpegVarietyFirstChild = jpegVariety.getFirstChild(); + if (jpegVarietyFirstChild != null) { + jpegVarietyFirstChild.appendChild(app2ICC); + } // new XMLSerializer(System.err, System.getProperty("file.encoding")).serialize(tree, false); + metadata.mergeTree(format, tree); + return metadata; } From cd6e9ebbf5de7cff9a62123816e026c20ab53dcb Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sun, 8 Sep 2013 14:43:51 +0200 Subject: [PATCH 17/98] TMS: Updated POM with new build-plugin version. --- servlet/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/servlet/pom.xml b/servlet/pom.xml index a59056e0..8e46c060 100644 --- a/servlet/pom.xml +++ b/servlet/pom.xml @@ -104,7 +104,7 @@ org.apache.maven.plugins maven-jar-plugin - 2.2 + 2.4 From d1f00ce81781d02619cef41fa2213ff25d69dede Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Fri, 13 Sep 2013 16:25:36 +0200 Subject: [PATCH 18/98] TMC-IOENC: Decoder implementation clean-up. --- .../io/enc/AbstractRLEDecoder.java | 32 +++++++++---------- .../twelvemonkeys/io/enc/Base64Decoder.java | 10 +++--- .../com/twelvemonkeys/io/enc/Decoder.java | 8 ++--- .../twelvemonkeys/io/enc/DecoderStream.java | 4 +-- .../io/enc/PackBits16Decoder.java | 20 ++++++------ .../twelvemonkeys/io/enc/PackBitsDecoder.java | 22 ++++++------- .../com/twelvemonkeys/io/enc/RLE4Decoder.java | 4 +-- .../com/twelvemonkeys/io/enc/RLE8Decoder.java | 4 +-- .../twelvemonkeys/io/enc/InflateDecoder.java | 6 ++-- 9 files changed, 54 insertions(+), 56 deletions(-) diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java index 9a427220..a43711a8 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java @@ -50,13 +50,13 @@ abstract class AbstractRLEDecoder implements Decoder { protected int dstY; /** - * Creates an RLEDecoder. As RLE encoded BMP's may contain x and y deltas, + * Creates an RLEDecoder. As RLE encoded BMPs may contain x and y deltas, * etc, we need to know height and width of the image. * * @param pWidth width of the image - * @param pHeight heigth of the image + * @param pHeight height of the image */ - AbstractRLEDecoder(int pWidth, int pHeight) { + AbstractRLEDecoder(final int pWidth, final int pHeight) { width = pWidth; int bytesPerRow = width; int mod = bytesPerRow % 4; @@ -77,32 +77,32 @@ abstract class AbstractRLEDecoder implements Decoder { /** * Decodes one full row of image data. * - * @param pStream the input stream containint RLE data + * @param pStream the input stream containing RLE data * - * @throws IOException if an I/O related exception ocurs while reading + * @throws IOException if an I/O related exception occurs while reading */ - protected abstract void decodeRow(InputStream pStream) throws IOException; + protected abstract void decodeRow(final InputStream pStream) throws IOException; /** * Decodes as much data as possible, from the stream into the buffer. * - * @param pStream the input stream containing RLE data - * @param pBuffer the buffer to decode the data to + * @param stream the input stream containing RLE data + * @param buffer the buffer to decode the data to * * @return the number of bytes decoded from the stream, to the buffer * * @throws IOException if an I/O related exception ocurs while reading */ - public final int decode(InputStream pStream, ByteBuffer pBuffer) throws IOException { - while (pBuffer.hasRemaining() && dstY >= 0) { + public final int decode(final InputStream stream, final ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining() && dstY >= 0) { // NOTE: Decode only full rows, don't decode if y delta if (dstX == 0 && srcY == dstY) { - decodeRow(pStream); + decodeRow(stream); } - int length = Math.min(row.length - dstX, pBuffer.remaining()); -// System.arraycopy(row, dstX, pBuffer, decoded, length); - pBuffer.put(row, 0, length); + int length = Math.min(row.length - dstX, buffer.remaining()); +// System.arraycopy(row, dstX, buffer, decoded, length); + buffer.put(row, 0, length); dstX += length; // decoded += length; @@ -120,7 +120,7 @@ abstract class AbstractRLEDecoder implements Decoder { } } - return pBuffer.position(); + return buffer.position(); } /** @@ -131,7 +131,7 @@ abstract class AbstractRLEDecoder implements Decoder { * * @throws EOFException if {@code pByte} is negative */ - protected static int checkEOF(int pByte) throws EOFException { + protected static int checkEOF(final int pByte) throws EOFException { if (pByte < 0) { throw new EOFException("Premature end of file"); } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java index dc9f319a..fe73e861 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java @@ -164,23 +164,23 @@ public final class Base64Decoder implements Decoder { return true; } - public int decode(final InputStream pStream, final ByteBuffer pBuffer) throws IOException { + public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException { do { int k = 72; int i; for (i = 0; i + 4 < k; i += 4) { - if(!decodeAtom(pStream, pBuffer, 4)) { + if(!decodeAtom(stream, buffer, 4)) { break; } } - if (!decodeAtom(pStream, pBuffer, k - i)) { + if (!decodeAtom(stream, buffer, k - i)) { break; } } - while (pBuffer.remaining() > 54); // 72 char lines should produce no more than 54 bytes + while (buffer.remaining() > 54); // 72 char lines should produce no more than 54 bytes - return pBuffer.position(); + return buffer.position(); } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Decoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Decoder.java index 0ceda346..7bd83879 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Decoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Decoder.java @@ -48,11 +48,11 @@ import java.nio.ByteBuffer; public interface Decoder { /** - * Decodes up to {@code pBuffer.length} bytes from the given input stream, + * Decodes up to {@code buffer.length} bytes from the given input stream, * into the given buffer. * - * @param pStream the input stream to decode data from - * @param pBuffer buffer to store the read data + * @param stream the input stream to decode data from + * @param buffer buffer to store the read data * * @return the total number of bytes read into the buffer, or {@code 0} * if there is no more data because the end of the stream has been reached. @@ -61,5 +61,5 @@ public interface Decoder { * @throws IOException if an I/O error occurs * @throws java.io.EOFException if a premature end-of-file is encountered */ - int decode(InputStream pStream, ByteBuffer pBuffer) throws IOException; + int decode(InputStream stream, ByteBuffer buffer) throws IOException; } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java index 9a36ed15..b61ced3d 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java @@ -47,9 +47,6 @@ public final class DecoderStream extends FilterInputStream { protected final ByteBuffer buffer; protected final Decoder decoder; - // TODO: Consider replacing the wrapped input stream with a channel like this - // ReadableByteChannel inChannel = Channels.newChannel(stream); - /** * Creates a new decoder stream and chains it to the * input stream specified by the {@code pStream} argument. @@ -77,6 +74,7 @@ public final class DecoderStream extends FilterInputStream { */ public DecoderStream(final InputStream pStream, final Decoder pDecoder, final int pBufferSize) { super(pStream); + decoder = pDecoder; buffer = ByteBuffer.allocate(pBufferSize); buffer.flip(); diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java index a141d30f..7a7c3ad6 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java @@ -78,20 +78,20 @@ public final class PackBits16Decoder implements Decoder { /** * Decodes bytes from the given input stream, to the given buffer. * - * @param pStream the stream to decode from - * @param pBuffer a byte array, minimum 128 (or 129 if no-op is disabled) + * @param stream the stream to decode from + * @param buffer a byte array, minimum 128 (or 129 if no-op is disabled) * bytes long * @return The number of bytes decoded * * @throws java.io.IOException */ - public int decode(final InputStream pStream, final ByteBuffer pBuffer) throws IOException { + public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException { if (reachedEOF) { return -1; } int read = 0; - final int max = pBuffer.capacity(); + final int max = buffer.capacity(); while (read < max) { int n; @@ -103,7 +103,7 @@ public final class PackBits16Decoder implements Decoder { } else { // Start new run - int b = pStream.read(); + int b = stream.read(); if (b < 0) { reachedEOF = true; break; @@ -127,18 +127,18 @@ public final class PackBits16Decoder implements Decoder { if (n >= 0) { // Copy next n + 1 shorts literally int len = 2 * (n + 1); - readFully(pStream, pBuffer, len); + readFully(stream, buffer, len); read += len; } // Allow -128 for compatibility, see above else if (disableNoop || n != -128) { // Replicate the next short -n + 1 times - byte value1 = readByte(pStream); - byte value2 = readByte(pStream); + byte value1 = readByte(stream); + byte value2 = readByte(stream); for (int i = -n + 1; i > 0; i--) { - pBuffer.put(value1); - pBuffer.put(value2); + buffer.put(value1); + buffer.put(value2); } } // else NOOP (-128) diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java index 9cb9ad73..0c173754 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java @@ -93,19 +93,19 @@ public final class PackBitsDecoder implements Decoder { /** * Decodes bytes from the given input stream, to the given buffer. * - * @param pStream the stream to decode from - * @param pBuffer a byte array, minimum 128 (or 129 if no-op is disabled) bytes long + * @param stream the stream to decode from + * @param buffer a byte array, minimum 128 (or 129 if no-op is disabled) bytes long * @return The number of bytes decoded * * @throws java.io.IOException */ - public int decode(final InputStream pStream, final ByteBuffer pBuffer) throws IOException { + public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException { if (reachedEOF) { return -1; } // TODO: Don't decode more than single runs, because some writers add pad bytes inside the stream... - while (pBuffer.hasRemaining()) { + while (buffer.hasRemaining()) { int n; if (splitRun) { @@ -115,7 +115,7 @@ public final class PackBitsDecoder implements Decoder { } else { // Start new run - int b = pStream.read(); + int b = stream.read(); if (b < 0) { reachedEOF = true; break; @@ -124,12 +124,12 @@ public final class PackBitsDecoder implements Decoder { } // Split run at or before max - if (n >= 0 && n + 1 > pBuffer.remaining()) { + if (n >= 0 && n + 1 > buffer.remaining()) { leftOfRun = n; splitRun = true; break; } - else if (n < 0 && -n + 1 > pBuffer.remaining()) { + else if (n < 0 && -n + 1 > buffer.remaining()) { leftOfRun = n; splitRun = true; break; @@ -138,15 +138,15 @@ public final class PackBitsDecoder implements Decoder { try { if (n >= 0) { // Copy next n + 1 bytes literally - readFully(pStream, pBuffer, n + 1); + readFully(stream, buffer, n + 1); } // Allow -128 for compatibility, see above else if (disableNoop || n != -128) { // Replicate the next byte -n + 1 times - byte value = readByte(pStream); + byte value = readByte(stream); for (int i = -n + 1; i > 0; i--) { - pBuffer.put(value); + buffer.put(value); } } // else NOOP (-128) @@ -156,7 +156,7 @@ public final class PackBitsDecoder implements Decoder { } } - return pBuffer.position(); + return buffer.position(); } static byte readByte(final InputStream pStream) throws IOException { diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/RLE4Decoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/RLE4Decoder.java index 6ea57d85..fc902061 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/RLE4Decoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/RLE4Decoder.java @@ -32,7 +32,7 @@ import java.io.InputStream; import java.io.IOException; /** - * Implements 4 bit RLE decoding as specifed by in the Windows BMP (aka DIB) file format. + * Implements 4 bit RLE decoding as specified by in the Windows BMP (aka DIB) file format. *

* * @author Harald Kuhr @@ -41,7 +41,7 @@ import java.io.IOException; // TODO: Move to other package or make public final class RLE4Decoder extends AbstractRLEDecoder { - public RLE4Decoder(int pWidth, int pHeight) { + public RLE4Decoder(final int pWidth, final int pHeight) { super((pWidth + 1) / 2, pHeight); } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/RLE8Decoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/RLE8Decoder.java index bc75dd2e..7529ba29 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/RLE8Decoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/RLE8Decoder.java @@ -32,7 +32,7 @@ import java.io.InputStream; import java.io.IOException; /** - * Implements 8 bit RLE decoding as specifed by in the Windows BMP (aka DIB) file format. + * Implements 8 bit RLE decoding as specified by in the Windows BMP (aka DIB) file format. *

* * @author Harald Kuhr @@ -41,7 +41,7 @@ import java.io.IOException; // TODO: Move to other package or make public final class RLE8Decoder extends AbstractRLEDecoder { - public RLE8Decoder(int pWidth, int pHeight) { + public RLE8Decoder(final int pWidth, final int pHeight) { super(pWidth, pHeight); } diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java index 15c9c7a1..e69b8b68 100644 --- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java @@ -76,17 +76,17 @@ final class InflateDecoder implements Decoder { buffer = new byte[1024]; } - public int decode(final InputStream pStream, final ByteBuffer pBuffer) throws IOException { + public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException { try { int decoded; - while ((decoded = inflater.inflate(pBuffer.array(), pBuffer.arrayOffset(), pBuffer.capacity())) == 0) { + while ((decoded = inflater.inflate(buffer.array(), buffer.arrayOffset(), buffer.capacity())) == 0) { if (inflater.finished() || inflater.needsDictionary()) { return 0; } if (inflater.needsInput()) { - fill(pStream); + fill(stream); } } From aebfad914f6182c6958639a8c896328b7bbd0609 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Fri, 13 Sep 2013 17:04:10 +0200 Subject: [PATCH 19/98] TMC-IOENC: Encoder implementation clean-up. --- .../java/com/twelvemonkeys/io/enc/Base64.java | 671 ------------------ .../twelvemonkeys/io/enc/Base64Decoder.java | 2 +- .../twelvemonkeys/io/enc/Base64Encoder.java | 66 +- .../com/twelvemonkeys/io/enc/Encoder.java | 9 +- .../twelvemonkeys/io/enc/EncoderStream.java | 27 +- .../twelvemonkeys/io/enc/PackBitsEncoder.java | 26 +- .../io/enc/Base64EncoderTestCase.java | 17 +- .../io/enc/EncoderAbstractTestCase.java | 2 +- .../twelvemonkeys/io/enc/DeflateEncoder.java | 7 +- 9 files changed, 70 insertions(+), 757 deletions(-) delete mode 100755 common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64.java diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64.java deleted file mode 100755 index 061a124b..00000000 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64.java +++ /dev/null @@ -1,671 +0,0 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * 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 "TwelveMonkeys" 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. - */ -/* - * Copyright (c) 2004, Mikael Grev, MiG InfoCom AB. (base64 @ miginfocom . com) - * 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 the MiG InfoCom AB 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 com.twelvemonkeys.io.enc; - -import java.util.Arrays; - -/** - * A very fast and memory efficient class to encode and decode to and from - * BASE64 in full accordance with RFC 2045. - *

- * On Windows XP sp1 with 1.4.2_04 and later ;), this encoder and decoder is - * about 10 times faster on small arrays (10 - 1000 bytes) and 2-3 times as fast - * on larger arrays (10000 - 1000000 bytes) compared to - * {@code sun.misc.Encoder()/Decoder()}. - *

- * On byte arrays the encoder is about 20% faster than - * Jakarta Commons Base64 Codec - * for encode and about 50% faster for decoding large arrays. This - * implementation is about twice as fast on very small arrays (< 30 bytes). - * If source/destination is a {@code String} this version is about three times - * as fast due to the fact that the Commons Codec result has to be recoded - * to a {@code String} from {@code byte[]}, which is very expensive. - *

- * This encode/decode algorithm doesn't create any temporary arrays as many - * other codecs do, it only allocates the resulting array. This produces less - * garbage and it is possible to handle arrays twice as large as algorithms that - * create a temporary array. (E.g. Jakarta Commons Codec). It is unknown - * whether Sun's {@code sun.misc.Encoder()/Decoder()} produce temporary arrays - * but since performance is quite low it probably does. - *

- * The encoder produces the same output as the Sun one except that Sun's encoder - * appends a trailing line separator if the last character isn't a pad. - * Unclear why but it only adds to the length and is probably a side effect. - * Both are in conformance with RFC 2045 though.
- * Commons codec seem to always add a trailing line separator. - *

- * Note! - * The encode/decode method pairs (types) come in three versions with the - * exact same algorithm and thus a lot of code redundancy. This is to not - * create any temporary arrays for transcoding to/from different - * format types. The methods not used can simply be commented out. - *

- * There is also a "fast" version of all decode methods that works the same way - * as the normal ones, but har a few demands on the decoded input. Normally - * though, these fast verions should be used if the source if - * the input is known and it hasn't bee tampered with. - *

- * If you find the code useful or you find a bug, please send me a note at - * base64 @ miginfocom . com. - *

- * - * @author Mikael Grev, 2004-aug-02 11:31:11 - * @version 2.2 - */ -final class Base64 { - private static final char[] CA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); - private static final int[] IA = new int[256]; - - static { - Arrays.fill(IA, -1); - for (int i = 0, iS = CA.length; i < iS; i++) { - IA[CA[i]] = i; - } - IA['='] = 0; - } - - // **************************************************************************************** - // * char[] version - // **************************************************************************************** - - /** - * Encodes a raw byte array into a BASE64 {@code char[]} representation im - * accordance with RFC 2045. - * - * @param sArr The bytes to convert. If {@code null} or length 0 an - * empty array will be returned. - * @param lineSep Optional "\r\n" after 76 characters, unless end of file. - *
- * No line separator will be in breach of RFC 2045 which - * specifies max 76 per line but will be a little faster. - * @return A BASE64 encoded array. Never {@code null}. - */ - public static char[] encodeToChar(byte[] sArr, boolean lineSep) { - // Check special case - int sLen = sArr != null ? sArr.length : 0; - if (sLen == 0) { - return new char[0]; - } - - int eLen = (sLen / 3) * 3;// Length of even 24-bits. - int cCnt = ((sLen - 1) / 3 + 1) << 2;// Returned character count - int dLen = cCnt + (lineSep ? (cCnt - 1) / 76 << 1 : 0);// Length of returned array - char[] dArr = new char[dLen]; - - // Encode even 24-bits - for (int s = 0, d = 0, cc = 0; s < eLen;) { - // Copy next three bytes into lower 24 bits of int, paying attension to sign. - int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff); - - // Encode the int into four chars - dArr[d++] = CA[(i >>> 18) & 0x3f]; - dArr[d++] = CA[(i >>> 12) & 0x3f]; - dArr[d++] = CA[(i >>> 6) & 0x3f]; - dArr[d++] = CA[i & 0x3f]; - - // Add optional line separator - if (lineSep && ++cc == 19 && d < dLen - 2) { - dArr[d++] = '\r'; - dArr[d++] = '\n'; - cc = 0; - } - } - - // Pad and encode last bits if source isn't even 24 bits. - int left = sLen - eLen;// 0 - 2. - if (left > 0) { - // Prepare the int - int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0); - - // Set last four chars - dArr[dLen - 4] = CA[i >> 12]; - dArr[dLen - 3] = CA[(i >>> 6) & 0x3f]; - dArr[dLen - 2] = left == 2 ? CA[i & 0x3f] : '='; - dArr[dLen - 1] = '='; - } - return dArr; - } - - /** - * Decodes a BASE64 encoded char array. All illegal characters will be - * ignored and can handle both arrays with and without line separators. - * - * @param sArr The source array. {@code null} or length 0 will return - * an empty array. - * @return The decoded array of bytes. May be of length 0. Will be - * {@code null} if the legal characters (including '=') isn't - * divideable by 4. (I.e. definitely corrupted). - */ - public static byte[] decode(char[] sArr) { - // Check special case - int sLen = sArr != null ? sArr.length : 0; - if (sLen == 0) { - return new byte[0]; - } - - // Count illegal characters (including '\r', '\n') to know what size the returned array will be, - // so we don't have to reallocate & copy it later. - int sepCnt = 0;// Number of separator characters. (Actually illegal characters, but that's a bonus...) - for (int i = 0; i < sLen; i++)// If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out. - { - if (IA[sArr[i]] < 0) { - sepCnt++; - } - } - - // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045. - if ((sLen - sepCnt) % 4 != 0) { - return null; - } - - int pad = 0; - for (int i = sLen; i > 1 && IA[sArr[--i]] <= 0;) { - if (sArr[i] == '=') { - pad++; - } - } - - int len = ((sLen - sepCnt) * 6 >> 3) - pad; - - byte[] dArr = new byte[len];// Preallocate byte[] of exact length - - for (int s = 0, d = 0; d < len;) { - // Assemble three bytes into an int from four "valid" characters. - int i = 0; - for (int j = 0; j < 4; j++) - {// j only increased if a valid char was found. - int c = IA[sArr[s++]]; - if (c >= 0) { - i |= c << (18 - j * 6); - } - else { - j--; - } - } - // Add the bytes - dArr[d++] = (byte) (i >> 16); - if (d < len) { - dArr[d++] = (byte) (i >> 8); - if (d < len) { - dArr[d++] = (byte) i; - } - } - } - return dArr; - } - - /** - * Decodes a BASE64 encoded char array that is known to be resonably well formatted. The method is about twice as - * fast as {@link #decode(char[])}. The preconditions are:
- * + The array must have a line length of 76 chars OR no line separators at all (one line).
- * + Line separator must be "\r\n", as specified in RFC 2045 - * + The array must not contain illegal characters within the encoded string
- * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.
- * - * @param sArr The source array. Length 0 will return an empty array. {@code null} will throw an exception. - * @return The decoded array of bytes. May be of length 0. - */ - public static byte[] decodeFast(char[] sArr) { - // Check special case - int sLen = sArr.length; - if (sLen == 0) { - return new byte[0]; - } - - int sIx = 0, eIx = sLen - 1;// Start and end index after trimming. - - // Trim illegal chars from start - while (sIx < eIx && IA[sArr[sIx]] < 0) { - sIx++; - } - - // Trim illegal chars from end - while (eIx > 0 && IA[sArr[eIx]] < 0) { - eIx--; - } - - // get the padding count (=) (0, 1 or 2) - int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0;// Count '=' at end. - int cCnt = eIx - sIx + 1;// Content count including possible separators - int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0; - - int len = ((cCnt - sepCnt) * 6 >> 3) - pad;// The number of decoded bytes - byte[] dArr = new byte[len];// Preallocate byte[] of exact length - - // Decode all but the last 0 - 2 bytes. - int d = 0; - for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) { - // Assemble three bytes into an int from four "valid" characters. - int i = IA[sArr[sIx++]] << 18 | IA[sArr[sIx++]] << 12 | IA[sArr[sIx++]] << 6 | IA[sArr[sIx++]]; - - // Add the bytes - dArr[d++] = (byte) (i >> 16); - dArr[d++] = (byte) (i >> 8); - dArr[d++] = (byte) i; - - // If line separator, jump over it. - if (sepCnt > 0 && ++cc == 19) { - sIx += 2; - cc = 0; - } - } - - if (d < len) { - // Decode last 1-3 bytes (incl '=') into 1-3 bytes - int i = 0; - for (int j = 0; sIx <= eIx - pad; j++) { - i |= IA[sArr[sIx++]] << (18 - j * 6); - } - - for (int r = 16; d < len; r -= 8) { - dArr[d++] = (byte) (i >> r); - } - } - - return dArr; - } - - // **************************************************************************************** - // * byte[] version - // **************************************************************************************** - - /** - * Encodes a raw byte array into a BASE64 {@code byte[]} representation i accordance with RFC 2045. - * - * @param sArr The bytes to convert. If {@code null} or length 0 an empty array will be returned. - * @param lineSep Optional "\r\n" after 76 characters, unless end of file.
- * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a - * little faster. - * @return A BASE64 encoded array. Never {@code null}. - */ - public static byte[] encodeToByte(byte[] sArr, boolean lineSep) { - // Check special case - int sLen = sArr != null ? sArr.length : 0; - if (sLen == 0) { - return new byte[0]; - } - - int eLen = (sLen / 3) * 3;// Length of even 24-bits. - int cCnt = ((sLen - 1) / 3 + 1) << 2;// Returned character count - int dLen = cCnt + (lineSep ? (cCnt - 1) / 76 << 1 : 0);// Length of returned array - byte[] dArr = new byte[dLen]; - - // Encode even 24-bits - for (int s = 0, d = 0, cc = 0; s < eLen;) { - // Copy next three bytes into lower 24 bits of int, paying attension to sign. - int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff); - - // Encode the int into four chars - dArr[d++] = (byte) CA[(i >>> 18) & 0x3f]; - dArr[d++] = (byte) CA[(i >>> 12) & 0x3f]; - dArr[d++] = (byte) CA[(i >>> 6) & 0x3f]; - dArr[d++] = (byte) CA[i & 0x3f]; - - // Add optional line separator - if (lineSep && ++cc == 19 && d < dLen - 2) { - dArr[d++] = '\r'; - dArr[d++] = '\n'; - cc = 0; - } - } - - // Pad and encode last bits if source isn't an even 24 bits. - int left = sLen - eLen;// 0 - 2. - if (left > 0) { - // Prepare the int - int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0); - - // Set last four chars - dArr[dLen - 4] = (byte) CA[i >> 12]; - dArr[dLen - 3] = (byte) CA[(i >>> 6) & 0x3f]; - dArr[dLen - 2] = left == 2 ? (byte) CA[i & 0x3f] : (byte) '='; - dArr[dLen - 1] = '='; - } - return dArr; - } - - /** - * Decodes a BASE64 encoded byte array. All illegal characters will be ignored and can handle both arrays with - * and without line separators. - * - * @param sArr The source array. Length 0 will return an empty array. {@code null} will throw an exception. - * @return The decoded array of bytes. May be of length 0. Will be {@code null} if the legal characters - * (including '=') isn't divideable by 4. (I.e. definitely corrupted). - */ - public static byte[] decode(byte[] sArr) { - // Check special case - int sLen = sArr.length; - - // Count illegal characters (including '\r', '\n') to know what size the returned array will be, - // so we don't have to reallocate & copy it later. - int sepCnt = 0;// Number of separator characters. (Actually illegal characters, but that's a bonus...) - for (int i = 0; i < sLen; i++)// If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out. - { - if (IA[sArr[i] & 0xff] < 0) { - sepCnt++; - } - } - - // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045. - if ((sLen - sepCnt) % 4 != 0) { - return null; - } - - int pad = 0; - for (int i = sLen; i > 1 && IA[sArr[--i] & 0xff] <= 0;) { - if (sArr[i] == '=') { - pad++; - } - } - - int len = ((sLen - sepCnt) * 6 >> 3) - pad; - - byte[] dArr = new byte[len];// Preallocate byte[] of exact length - - for (int s = 0, d = 0; d < len;) { - // Assemble three bytes into an int from four "valid" characters. - int i = 0; - for (int j = 0; j < 4; j++) - {// j only increased if a valid char was found. - int c = IA[sArr[s++] & 0xff]; - if (c >= 0) { - i |= c << (18 - j * 6); - } - else { - j--; - } - } - - // Add the bytes - dArr[d++] = (byte) (i >> 16); - if (d < len) { - dArr[d++] = (byte) (i >> 8); - if (d < len) { - dArr[d++] = (byte) i; - } - } - } - - return dArr; - } - - /** - * Decodes a BASE64 encoded byte array that is known to be resonably well formatted. The method is about twice as - * fast as {@link #decode(byte[])}. The preconditions are:
- * + The array must have a line length of 76 chars OR no line separators at all (one line).
- * + Line separator must be "\r\n", as specified in RFC 2045 - * + The array must not contain illegal characters within the encoded string
- * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.
- * - * @param sArr The source array. Length 0 will return an empty array. {@code null} will throw an exception. - * @return The decoded array of bytes. May be of length 0. - */ - public static byte[] decodeFast(byte[] sArr) { - // Check special case - int sLen = sArr.length; - if (sLen == 0) { - return new byte[0]; - } - - int sIx = 0, eIx = sLen - 1;// Start and end index after trimming. - - // Trim illegal chars from start - while (sIx < eIx && IA[sArr[sIx] & 0xff] < 0) { - sIx++; - } - - // Trim illegal chars from end - while (eIx > 0 && IA[sArr[eIx] & 0xff] < 0) { - eIx--; - } - - // get the padding count (=) (0, 1 or 2) - int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0;// Count '=' at end. - int cCnt = eIx - sIx + 1;// Content count including possible separators - int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0; - - int len = ((cCnt - sepCnt) * 6 >> 3) - pad;// The number of decoded bytes - byte[] dArr = new byte[len];// Preallocate byte[] of exact length - - // Decode all but the last 0 - 2 bytes. - int d = 0; - for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) { - // Assemble three bytes into an int from four "valid" characters. - int i = IA[sArr[sIx++]] << 18 | IA[sArr[sIx++]] << 12 | IA[sArr[sIx++]] << 6 | IA[sArr[sIx++]]; - - // Add the bytes - dArr[d++] = (byte) (i >> 16); - dArr[d++] = (byte) (i >> 8); - dArr[d++] = (byte) i; - - // If line separator, jump over it. - if (sepCnt > 0 && ++cc == 19) { - sIx += 2; - cc = 0; - } - } - - if (d < len) { - // Decode last 1-3 bytes (incl '=') into 1-3 bytes - int i = 0; - for (int j = 0; sIx <= eIx - pad; j++) { - i |= IA[sArr[sIx++]] << (18 - j * 6); - } - - for (int r = 16; d < len; r -= 8) { - dArr[d++] = (byte) (i >> r); - } - } - - return dArr; - } - - // **************************************************************************************** - // * String version - // **************************************************************************************** - - /** - * Encodes a raw byte array into a BASE64 {@code String} representation i accordance with RFC 2045. - * - * @param sArr The bytes to convert. If {@code null} or length 0 an empty array will be returned. - * @param lineSep Optional "\r\n" after 76 characters, unless end of file.
- * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a - * little faster. - * @return A BASE64 encoded array. Never {@code null}. - */ - public static String encodeToString(byte[] sArr, boolean lineSep) { - // Reuse char[] since we can't create a String incrementally anyway and StringBuffer/Builder would be slower. - return new String(encodeToChar(sArr, lineSep)); - } - - /** - * Decodes a BASE64 encoded {@code String}. All illegal characters will be ignored and can handle both strings with - * and without line separators.
- * Note! It can be up to about 2x the speed to call {@code decode(str.toCharArray())} instead. That - * will create a temporary array though. This version will use {@code str.charAt(i)} to iterate the string. - * - * @param str The source string. {@code null} or length 0 will return an empty array. - * @return The decoded array of bytes. May be of length 0. Will be {@code null} if the legal characters - * (including '=') isn't divideable by 4. (I.e. definitely corrupted). - */ - public static byte[] decode(String str) { - // Check special case - int sLen = str != null ? str.length() : 0; - if (sLen == 0) { - return new byte[0]; - } - - // Count illegal characters (including '\r', '\n') to know what size the returned array will be, - // so we don't have to reallocate & copy it later. - int sepCnt = 0;// Number of separator characters. (Actually illegal characters, but that's a bonus...) - for (int i = 0; i < sLen; i++)// If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out. - { - if (IA[str.charAt(i)] < 0) { - sepCnt++; - } - } - - // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045. - if ((sLen - sepCnt) % 4 != 0) { - return null; - } - - // Count '=' at end - int pad = 0; - for (int i = sLen; i > 1 && IA[str.charAt(--i)] <= 0;) { - if (str.charAt(i) == '=') { - pad++; - } - } - - int len = ((sLen - sepCnt) * 6 >> 3) - pad; - - byte[] dArr = new byte[len];// Preallocate byte[] of exact length - - for (int s = 0, d = 0; d < len;) { - // Assemble three bytes into an int from four "valid" characters. - int i = 0; - for (int j = 0; j < 4; j++) - {// j only increased if a valid char was found. - int c = IA[str.charAt(s++)]; - if (c >= 0) { - i |= c << (18 - j * 6); - } - else { - j--; - } - } - // Add the bytes - dArr[d++] = (byte) (i >> 16); - if (d < len) { - dArr[d++] = (byte) (i >> 8); - if (d < len) { - dArr[d++] = (byte) i; - } - } - } - return dArr; - } - - /** - * Decodes a BASE64 encoded string that is known to be resonably well formatted. The method is about twice as - * fast as {@link #decode(String)}. The preconditions are:
- * + The array must have a line length of 76 chars OR no line separators at all (one line).
- * + Line separator must be "\r\n", as specified in RFC 2045 - * + The array must not contain illegal characters within the encoded string
- * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.
- * - * @param s The source string. Length 0 will return an empty array. {@code null} will throw an exception. - * @return The decoded array of bytes. May be of length 0. - */ - public static byte[] decodeFast(String s) { - // Check special case - int sLen = s.length(); - if (sLen == 0) { - return new byte[0]; - } - - int sIx = 0, eIx = sLen - 1;// Start and end index after trimming. - - // Trim illegal chars from start - while (sIx < eIx && IA[s.charAt(sIx) & 0xff] < 0) { - sIx++; - } - - // Trim illegal chars from end - while (eIx > 0 && IA[s.charAt(eIx) & 0xff] < 0) { - eIx--; - } - - // get the padding count (=) (0, 1 or 2) - int pad = s.charAt(eIx) == '=' ? (s.charAt(eIx - 1) == '=' ? 2 : 1) : 0;// Count '=' at end. - int cCnt = eIx - sIx + 1;// Content count including possible separators - int sepCnt = sLen > 76 ? (s.charAt(76) == '\r' ? cCnt / 78 : 0) << 1 : 0; - - int len = ((cCnt - sepCnt) * 6 >> 3) - pad;// The number of decoded bytes - byte[] dArr = new byte[len];// Preallocate byte[] of exact length - - // Decode all but the last 0 - 2 bytes. - int d = 0; - for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) { - // Assemble three bytes into an int from four "valid" characters. - int i = IA[s.charAt(sIx++)] << 18 | IA[s.charAt(sIx++)] << 12 | IA[s.charAt(sIx++)] << 6 | IA[s.charAt(sIx++)]; - - // Add the bytes - dArr[d++] = (byte) (i >> 16); - dArr[d++] = (byte) (i >> 8); - dArr[d++] = (byte) i; - - // If line separator, jump over it. - if (sepCnt > 0 && ++cc == 19) { - sIx += 2; - cc = 0; - } - } - - if (d < len) { - // Decode last 1-3 bytes (incl '=') into 1-3 bytes - int i = 0; - for (int j = 0; sIx <= eIx - pad; j++) { - i |= IA[s.charAt(sIx++)] << (18 - j * 6); - } - - for (int r = 16; d < len; r -= 8) { - dArr[d++] = (byte) (i >> r); - } - } - - return dArr; - } -} \ No newline at end of file diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java index fe73e861..7e385c3a 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java @@ -47,7 +47,7 @@ public final class Base64Decoder implements Decoder { /** * This array maps the characters to their 6 bit values */ - final static char[] PEM_ARRAY = { + final static byte[] PEM_ARRAY = { //0 1 2 3 4 5 6 7 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 0 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 1 diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Encoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Encoder.java index 54af6eb9..d8336498 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Encoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Encoder.java @@ -30,6 +30,7 @@ package com.twelvemonkeys.io.enc; import java.io.OutputStream; import java.io.IOException; +import java.nio.ByteBuffer; /** * {@code Encoder} implementation for standard base64 encoding. @@ -44,15 +45,9 @@ import java.io.IOException; */ public class Base64Encoder implements Encoder { - public void encode(final OutputStream pStream, final byte[] pBuffer, final int pOffset, final int pLength) + public void encode(final OutputStream stream, final ByteBuffer buffer) throws IOException { - if (pOffset < 0 || pOffset > pLength || pOffset > pBuffer.length) { - throw new IndexOutOfBoundsException("offset outside [0...length]"); - } - else if (pLength > pBuffer.length) { - throw new IndexOutOfBoundsException("length > buffer length"); - } // TODO: Implement // NOTE: This is impossible, given the current spec, as we need to either: @@ -61,48 +56,47 @@ public class Base64Encoder implements Encoder { // to ensure proper end of stream handling int length; - int offset = pOffset; // TODO: Temp impl, will only work for single writes - while ((pBuffer.length - offset) > 0) { + while (buffer.hasRemaining()) { byte a, b, c; - if ((pBuffer.length - offset) > 2) { - length = 3; - } - else { - length = pBuffer.length - offset; - } +// if ((buffer.remaining()) > 2) { +// length = 3; +// } +// else { +// length = buffer.remaining(); +// } + length = Math.min(3, buffer.remaining()); switch (length) { case 1: - a = pBuffer[offset]; + a = buffer.get(); b = 0; - pStream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]); - pStream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); - pStream.write('='); - pStream.write('='); - offset++; + stream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]); + stream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + stream.write('='); + stream.write('='); break; + case 2: - a = pBuffer[offset]; - b = pBuffer[offset + 1]; + a = buffer.get(); + b = buffer.get(); c = 0; - pStream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]); - pStream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); - pStream.write(Base64Decoder.PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); - pStream.write('='); - offset += offset + 2; // ??? + stream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]); + stream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + stream.write(Base64Decoder.PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + stream.write('='); break; + default: - a = pBuffer[offset]; - b = pBuffer[offset + 1]; - c = pBuffer[offset + 2]; - pStream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]); - pStream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); - pStream.write(Base64Decoder.PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); - pStream.write(Base64Decoder.PEM_ARRAY[c & 0x3F]); - offset = offset + 3; + a = buffer.get(); + b = buffer.get(); + c = buffer.get(); + stream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]); + stream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + stream.write(Base64Decoder.PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + stream.write(Base64Decoder.PEM_ARRAY[c & 0x3F]); break; } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Encoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Encoder.java index c6f45bc7..c1a126b0 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Encoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Encoder.java @@ -30,6 +30,7 @@ package com.twelvemonkeys.io.enc; import java.io.IOException; import java.io.OutputStream; +import java.nio.ByteBuffer; /** * Interface for endcoders. @@ -50,14 +51,12 @@ public interface Encoder { * Encodes up to {@code pBuffer.length} bytes into the given input stream, * from the given buffer. * - * @param pStream the outputstream to encode data to - * @param pBuffer buffer to read data from - * @param pOffset offset into the buffer array - * @param pLength length of data in the buffer + * @param stream the output stream to encode data to + * @param buffer buffer to read data from * * @throws java.io.IOException if an I/O error occurs */ - void encode(OutputStream pStream, byte[] pBuffer, int pOffset, int pLength) throws IOException; + void encode(OutputStream stream, ByteBuffer buffer) throws IOException; //TODO: int requiredBufferSize(): -1 == any, otherwise, use this buffer size // void flush()? diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java index 0a4f0fce..6cf9ee31 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java @@ -29,8 +29,9 @@ package com.twelvemonkeys.io.enc; import java.io.FilterOutputStream; -import java.io.OutputStream; import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; /** * An {@code OutputStream} that provides on-the-fly encoding to an underlying @@ -47,8 +48,7 @@ public final class EncoderStream extends FilterOutputStream { protected final Encoder encoder; private final boolean flushOnWrite; - protected int bufferPos; - protected final byte[] buffer; + protected final ByteBuffer buffer; /** * Creates an output stream filter built on top of the specified @@ -76,8 +76,8 @@ public final class EncoderStream extends FilterOutputStream { encoder = pEncoder; flushOnWrite = pFlushOnWrite; - buffer = new byte[1024]; - bufferPos = 0; + buffer = ByteBuffer.allocate(1024); + buffer.flip(); } public void close() throws IOException { @@ -91,12 +91,12 @@ public final class EncoderStream extends FilterOutputStream { } private void encodeBuffer() throws IOException { - if (bufferPos != 0) { + if (buffer.hasRemaining()) { // Make sure all remaining data in buffer is written to the stream - encoder.encode(out, buffer, 0, bufferPos); + encoder.encode(out, buffer); // Reset buffer - bufferPos = 0; + buffer.clear(); } } @@ -109,25 +109,24 @@ public final class EncoderStream extends FilterOutputStream { // that the encoder can't buffer. In that case, the encoder should probably // tell the EncoderStream how large buffer it prefers... public void write(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { - if (!flushOnWrite && bufferPos + pLength < buffer.length) { + if (!flushOnWrite && pLength < buffer.remaining()) { // Buffer data - System.arraycopy(pBytes, pOffset, buffer, bufferPos, pLength); - bufferPos += pLength; + buffer.put(pBytes, pOffset, pLength); } else { // Encode data already in the buffer encodeBuffer(); // Encode rest without buffering - encoder.encode(out, pBytes, pOffset, pLength); + encoder.encode(out, ByteBuffer.wrap(pBytes, pOffset, pLength)); } } public void write(final int pByte) throws IOException { - if (bufferPos >= buffer.length - 1) { + if (!buffer.hasRemaining()) { encodeBuffer(); // Resets bufferPos to 0 } - buffer[bufferPos++] = (byte) pByte; + buffer.put((byte) pByte); } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java index cfc80a31..61edf7bb 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java @@ -30,6 +30,7 @@ package com.twelvemonkeys.io.enc; import java.io.OutputStream; import java.io.IOException; +import java.nio.ByteBuffer; /** * Encoder implementation for Apple PackBits run-length encoding. @@ -71,53 +72,54 @@ public final class PackBitsEncoder implements Encoder { public PackBitsEncoder() { } - public void encode(OutputStream pStream, byte[] pBuffer, int pOffset, int pLength) throws IOException { + public void encode(final OutputStream stream, final ByteBuffer buffer) throws IOException { // NOTE: It's best to encode a 2 byte repeat // run as a replicate run except when preceded and followed by a // literal run, in which case it's best to merge the three into one // literal run. Always encode 3 byte repeats as replicate runs. // NOTE: Worst case: output = input + (input + 127) / 128 - int offset = pOffset; - final int max = pOffset + pLength - 1; + int offset = buffer.position(); + final int max = buffer.remaining() - 1; final int maxMinus1 = max - 1; + final byte[] pBuffer = buffer.array(); while (offset <= max) { // Compressed run int run = 1; byte replicate = pBuffer[offset]; - while(run < 127 && offset < max && pBuffer[offset] == pBuffer[offset + 1]) { + while (run < 127 && offset < max && pBuffer[offset] == pBuffer[offset + 1]) { offset++; run++; } if (run > 1) { offset++; - pStream.write(-(run - 1)); - pStream.write(replicate); + stream.write(-(run - 1)); + stream.write(replicate); } // Literal run run = 0; while ((run < 128 && ((offset < max && pBuffer[offset] != pBuffer[offset + 1]) || (offset < maxMinus1 && pBuffer[offset] != pBuffer[offset + 2])))) { - buffer[run++] = pBuffer[offset++]; + this.buffer[run++] = pBuffer[offset++]; } // If last byte, include it in literal run, if space if (offset == max && run > 0 && run < 128) { - buffer[run++] = pBuffer[offset++]; + this.buffer[run++] = pBuffer[offset++]; } if (run > 0) { - pStream.write(run - 1); - pStream.write(buffer, 0, run); + stream.write(run - 1); + stream.write(this.buffer, 0, run); } // If last byte, and not space, start new literal run if (offset == max && (run <= 0 || run >= 128)) { - pStream.write(0); - pStream.write(pBuffer[offset++]); + stream.write(0); + stream.write(pBuffer[offset++]); } } } diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/Base64EncoderTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/Base64EncoderTestCase.java index c1177538..0c3e7720 100644 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/Base64EncoderTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/Base64EncoderTestCase.java @@ -2,7 +2,9 @@ package com.twelvemonkeys.io.enc; import org.junit.Test; -import java.io.*; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; import static org.junit.Assert.*; @@ -23,19 +25,6 @@ public class Base64EncoderTestCase extends EncoderAbstractTestCase { return new Base64Decoder(); } - @Test - public void testNegativeEncode() throws IOException { - Encoder encoder = createEncoder(); - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - try { - encoder.encode(bytes, new byte[1], 2, 1); - fail("wrong index should throw IndexOutOfBoundsException"); - } - catch (IndexOutOfBoundsException expected) { - } - } - @Test public void testEmptyEncode() throws IOException { String data = ""; diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java index 8bf50fc2..0977a654 100644 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java @@ -35,7 +35,7 @@ public abstract class EncoderAbstractTestCase extends ObjectAbstractTestCase { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); try { - encoder.encode(bytes, null, 0, 1); + encoder.encode(bytes, null); fail("null should throw NullPointerException"); } catch (NullPointerException expected) { diff --git a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java index 8f1c6bf7..3c77949f 100644 --- a/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java @@ -30,6 +30,7 @@ package com.twelvemonkeys.io.enc; import java.io.OutputStream; import java.io.IOException; +import java.nio.ByteBuffer; import java.util.zip.Deflater; /** @@ -62,12 +63,12 @@ final class DeflateEncoder implements Encoder { deflater = pDeflater; } - public void encode(final OutputStream pStream, final byte[] pBuffer, final int pOffset, final int pLength) + public void encode(final OutputStream stream, ByteBuffer buffer) throws IOException { System.out.println("DeflateEncoder.encode"); - deflater.setInput(pBuffer, pOffset, pLength); - flushInputToStream(pStream); + deflater.setInput(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + flushInputToStream(stream); } private void flushInputToStream(final OutputStream pStream) throws IOException { From 602e5ec34bd8ab03bb9eae5fb4d3a8210d2690aa Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 18 Sep 2013 10:29:56 +0200 Subject: [PATCH 20/98] TMI-TIFF: Rewritten to use ByteBuffer. --- .../tiff/HorizontalDeDifferencingStream.java | 230 ++++++------------ 1 file changed, 74 insertions(+), 156 deletions(-) diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStream.java index 4adaf9ed..04b86dc0 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStream.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStream.java @@ -31,10 +31,12 @@ package com.twelvemonkeys.imageio.plugins.tiff; import com.twelvemonkeys.lang.Validate; import java.io.EOFException; -import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; /** * A decoder for data converted using "horizontal differencing predictor". @@ -43,29 +45,26 @@ import java.nio.ByteOrder; * @author last modified by $Author: haraldk$ * @version $Id: HorizontalDeDifferencingStream.java,v 1.0 11.03.13 14:20 haraldk Exp$ */ -final class HorizontalDeDifferencingStream extends FilterInputStream { +final class HorizontalDeDifferencingStream extends InputStream { // See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64. private final int columns; // NOTE: PlanarConfiguration == 2 may be treated as samplesPerPixel == 1 private final int samplesPerPixel; private final int bitsPerSample; - private final ByteOrder byteOrder; - int decodedLength; - int decodedPos; - - private final byte[] buffer; + private final ReadableByteChannel channel; + private final ByteBuffer buffer; public HorizontalDeDifferencingStream(final InputStream stream, final int columns, final int samplesPerPixel, final int bitsPerSample, final ByteOrder byteOrder) { - super(Validate.notNull(stream, "stream")); + channel = Channels.newChannel(Validate.notNull(stream, "stream")); this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0"); this.samplesPerPixel = Validate.isTrue(bitsPerSample >= 8 || samplesPerPixel == 1, samplesPerPixel, "Unsupported samples per pixel for < 8 bit samples: %s"); this.bitsPerSample = Validate.isTrue(isValidBPS(bitsPerSample), bitsPerSample, "Unsupported bits per sample value: %s"); - this.byteOrder = byteOrder; - buffer = new byte[(columns * samplesPerPixel * bitsPerSample + 7) / 8]; + buffer = ByteBuffer.allocate((columns * samplesPerPixel * bitsPerSample + 7) / 8).order(byteOrder); + buffer.flip(); } private boolean isValidBPS(final int bitsPerSample) { @@ -83,75 +82,81 @@ final class HorizontalDeDifferencingStream extends FilterInputStream { } } - private void fetch() throws IOException { - int pos = 0; - int read; + @SuppressWarnings("StatementWithEmptyBody") + private boolean fetch() throws IOException { + buffer.clear(); - // This *SHOULD* read an entire row of pixels (or nothing at all) into the buffer, otherwise we will throw EOFException below - while (pos < buffer.length && (read = in.read(buffer, pos, buffer.length - pos)) > 0) { - pos += read; - } + // This *SHOULD* read an entire row of pixels (or nothing at all) into the buffer, + // otherwise we will throw EOFException below + while (channel.read(buffer) > 0); - if (pos > 0) { - if (buffer.length > pos) { + if (buffer.position() > 0) { + if (buffer.hasRemaining()) { throw new EOFException("Unexpected end of stream"); } decodeRow(); + buffer.flip(); - decodedLength = buffer.length; - decodedPos = 0; + return true; } else { - decodedLength = -1; + buffer.position(buffer.capacity()); + + return false; } } private void decodeRow() throws EOFException { // Un-apply horizontal predictor + byte original; int sample = 0; byte temp; switch (bitsPerSample) { case 1: for (int b = 0; b < (columns + 7) / 8; b++) { - sample += (buffer[b] >> 7) & 0x1; + original = buffer.get(b); + sample += (original >> 7) & 0x1; temp = (byte) ((sample << 7) & 0x80); - sample += (buffer[b] >> 6) & 0x1; + sample += (original >> 6) & 0x1; temp |= (byte) ((sample << 6) & 0x40); - sample += (buffer[b] >> 5) & 0x1; + sample += (original >> 5) & 0x1; temp |= (byte) ((sample << 5) & 0x20); - sample += (buffer[b] >> 4) & 0x1; + sample += (original >> 4) & 0x1; temp |= (byte) ((sample << 4) & 0x10); - sample += (buffer[b] >> 3) & 0x1; + sample += (original >> 3) & 0x1; temp |= (byte) ((sample << 3) & 0x08); - sample += (buffer[b] >> 2) & 0x1; + sample += (original >> 2) & 0x1; temp |= (byte) ((sample << 2) & 0x04); - sample += (buffer[b] >> 1) & 0x1; + sample += (original >> 1) & 0x1; temp |= (byte) ((sample << 1) & 0x02); - sample += buffer[b] & 0x1; - buffer[b] = (byte) (temp | sample & 0x1); + sample += original & 0x1; + buffer.put(b, (byte) (temp | sample & 0x1)); } break; + case 2: for (int b = 0; b < (columns + 3) / 4; b++) { - sample += (buffer[b] >> 6) & 0x3; + original = buffer.get(b); + sample += (original >> 6) & 0x3; temp = (byte) ((sample << 6) & 0xc0); - sample += (buffer[b] >> 4) & 0x3; + sample += (original >> 4) & 0x3; temp |= (byte) ((sample << 4) & 0x30); - sample += (buffer[b] >> 2) & 0x3; + sample += (original >> 2) & 0x3; temp |= (byte) ((sample << 2) & 0x0c); - sample += buffer[b] & 0x3; - buffer[b] = (byte) (temp | sample & 0x3); + sample += original & 0x3; + buffer.put(b, (byte) (temp | sample & 0x3)); } break; case 4: for (int b = 0; b < (columns + 1) / 2; b++) { - sample += (buffer[b] >> 4) & 0xf; + original = buffer.get(b); + sample += (original >> 4) & 0xf; temp = (byte) ((sample << 4) & 0xf0); - sample += buffer[b] & 0x0f; - buffer[b] = (byte) (temp | sample & 0xf); + sample += original & 0x0f; + buffer.put(b, (byte) (temp | sample & 0xf)); } break; @@ -159,7 +164,7 @@ final class HorizontalDeDifferencingStream extends FilterInputStream { for (int x = 1; x < columns; x++) { for (int b = 0; b < samplesPerPixel; b++) { int off = x * samplesPerPixel + b; - buffer[off] = (byte) (buffer[off - samplesPerPixel] + buffer[off]); + buffer.put(off, (byte) (buffer.get(off - samplesPerPixel) + buffer.get(off))); } } break; @@ -168,7 +173,7 @@ final class HorizontalDeDifferencingStream extends FilterInputStream { for (int x = 1; x < columns; x++) { for (int b = 0; b < samplesPerPixel; b++) { int off = x * samplesPerPixel + b; - putShort(off, asShort(off - samplesPerPixel) + asShort(off)); + buffer.putShort(2 * off, (short) (buffer.getShort(2 * (off - samplesPerPixel)) + buffer.getShort(2 * off))); } } break; @@ -177,7 +182,7 @@ final class HorizontalDeDifferencingStream extends FilterInputStream { for (int x = 1; x < columns; x++) { for (int b = 0; b < samplesPerPixel; b++) { int off = x * samplesPerPixel + b; - putInt(off, asInt(off - samplesPerPixel) + asInt(off)); + buffer.putInt(4 * off, buffer.getInt(4 * (off - samplesPerPixel)) + buffer.getInt(4 * off)); } } break; @@ -186,7 +191,7 @@ final class HorizontalDeDifferencingStream extends FilterInputStream { for (int x = 1; x < columns; x++) { for (int b = 0; b < samplesPerPixel; b++) { int off = x * samplesPerPixel + b; - putLong(off, asLong(off - samplesPerPixel) + asLong(off)); + buffer.putLong(8 * off, buffer.getLong(8 * (off - samplesPerPixel)) + buffer.getLong(8 * off)); } } break; @@ -196,145 +201,58 @@ final class HorizontalDeDifferencingStream extends FilterInputStream { } } - private void putLong(final int index, final long value) { - if (byteOrder == ByteOrder.BIG_ENDIAN) { - buffer[index * 8 ] = (byte) ((value >> 56) & 0xff); - buffer[index * 8 + 1] = (byte) ((value >> 48) & 0xff); - buffer[index * 8 + 2] = (byte) ((value >> 40) & 0xff); - buffer[index * 8 + 3] = (byte) ((value >> 32) & 0xff); - buffer[index * 8 + 4] = (byte) ((value >> 24) & 0xff); - buffer[index * 8 + 5] = (byte) ((value >> 16) & 0xff); - buffer[index * 8 + 6] = (byte) ((value >> 8) & 0xff); - buffer[index * 8 + 7] = (byte) ((value) & 0xff); - } - else { - buffer[index * 8 + 7] = (byte) ((value >> 56) & 0xff); - buffer[index * 8 + 6] = (byte) ((value >> 48) & 0xff); - buffer[index * 8 + 5] = (byte) ((value >> 40) & 0xff); - buffer[index * 8 + 4] = (byte) ((value >> 32) & 0xff); - buffer[index * 8 + 3] = (byte) ((value >> 24) & 0xff); - buffer[index * 8 + 2] = (byte) ((value >> 16) & 0xff); - buffer[index * 8 + 1] = (byte) ((value >> 8) & 0xff); - buffer[index * 8 ] = (byte) ((value) & 0xff); - } - } - - private long asLong(final int index) { - if (byteOrder == ByteOrder.BIG_ENDIAN) { - return (buffer[index * 8 ] & 0xffl) << 56l | (buffer[index * 8 + 1] & 0xffl) << 48l | - (buffer[index * 8 + 2] & 0xffl) << 40l | (buffer[index * 8 + 3] & 0xffl) << 32l | - (buffer[index * 8 + 4] & 0xffl) << 24 | (buffer[index * 8 + 5] & 0xffl) << 16 | - (buffer[index * 8 + 6] & 0xffl) << 8 | buffer[index * 8 + 7] & 0xffl; - } - else { - return (buffer[index * 8 + 7] & 0xffl) << 56l | (buffer[index * 8 + 6] & 0xffl) << 48l | - (buffer[index * 8 + 5] & 0xffl) << 40l | (buffer[index * 8 + 4] & 0xffl) << 32l | - (buffer[index * 8 + 3] & 0xffl) << 24 | (buffer[index * 8 + 2] & 0xffl) << 16 | - (buffer[index * 8 + 1] & 0xffl) << 8 | buffer[index * 8] & 0xffl; - } - } - - private void putInt(final int index, final int value) { - if (byteOrder == ByteOrder.BIG_ENDIAN) { - buffer[index * 4 ] = (byte) ((value >> 24) & 0xff); - buffer[index * 4 + 1] = (byte) ((value >> 16) & 0xff); - buffer[index * 4 + 2] = (byte) ((value >> 8) & 0xff); - buffer[index * 4 + 3] = (byte) ((value) & 0xff); - } - else { - buffer[index * 4 + 3] = (byte) ((value >> 24) & 0xff); - buffer[index * 4 + 2] = (byte) ((value >> 16) & 0xff); - buffer[index * 4 + 1] = (byte) ((value >> 8) & 0xff); - buffer[index * 4 ] = (byte) ((value) & 0xff); - } - } - - private int asInt(final int index) { - if (byteOrder == ByteOrder.BIG_ENDIAN) { - return (buffer[index * 4] & 0xff) << 24 | (buffer[index * 4 + 1] & 0xff) << 16 | - (buffer[index * 4 + 2] & 0xff) << 8 | buffer[index * 4 + 3] & 0xff; - } - else { - return (buffer[index * 4 + 3] & 0xff) << 24 | (buffer[index * 4 + 2] & 0xff) << 16 | - (buffer[index * 4 + 1] & 0xff) << 8 | buffer[index * 4] & 0xff; - } - } - - private void putShort(final int index, final int value) { - if (byteOrder == ByteOrder.BIG_ENDIAN) { - buffer[index * 2 ] = (byte) ((value >> 8) & 0xff); - buffer[index * 2 + 1] = (byte) ((value) & 0xff); - } - else { - buffer[index * 2 + 1] = (byte) ((value >> 8) & 0xff); - buffer[index * 2 ] = (byte) ((value) & 0xff); - } - } - - private short asShort(final int index) { - if (byteOrder == ByteOrder.BIG_ENDIAN) { - return (short) ((buffer[index * 2] & 0xff) << 8 | buffer[index * 2 + 1] & 0xff); - } - else { - return (short) ((buffer[index * 2 + 1] & 0xff) << 8 | buffer[index * 2] & 0xff); - } - } - @Override public int read() throws IOException { - if (decodedLength < 0) { - return -1; - } - - if (decodedPos >= decodedLength) { - fetch(); - - if (decodedLength < 0) { + if (!buffer.hasRemaining()) { + if (!fetch()) { return -1; } } - return buffer[decodedPos++] & 0xff; + return buffer.get() & 0xff; } @Override public int read(byte[] b, int off, int len) throws IOException { - if (decodedLength < 0) { - return -1; - } - - if (decodedPos >= decodedLength) { - fetch(); - - if (decodedLength < 0) { + if (!buffer.hasRemaining()) { + if (!fetch()) { return -1; } } - int read = Math.min(decodedLength - decodedPos, len); - System.arraycopy(buffer, decodedPos, b, off, read); - decodedPos += read; + int read = Math.min(buffer.remaining(), len); + buffer.get(b, off, read); return read; } @Override public long skip(long n) throws IOException { - if (decodedLength < 0) { - return -1; + if (n < 0) { + return 0; } - if (decodedPos >= decodedLength) { - fetch(); - - if (decodedLength < 0) { - return -1; + if (!buffer.hasRemaining()) { + if (!fetch()) { + return 0; // SIC } } - int skipped = (int) Math.min(decodedLength - decodedPos, n); - decodedPos += skipped; + int skipped = (int) Math.min(buffer.remaining(), n); + buffer.position(buffer.position() + skipped); return skipped; } + + @Override + public void close() throws IOException { + try { + super.close(); + } + finally { + if (channel.isOpen()) { + channel.close(); + } + } + } } From 086357694aa3a96ee46b21f649fdb3b5b9a171f3 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 19 Sep 2013 09:25:59 +0200 Subject: [PATCH 21/98] TMI-JPEG-10: Fixed an issue with JPEGs without JFIF segment being treated as RGB, even when YCbCr. --- .../imageio/plugins/jpeg/JPEGImageReader.java | 22 +++++------- .../plugins/jpeg/JPEGImageReaderTest.java | 32 ++++++++++++++++++ .../src/test/resources/jpeg/no-jfif-ycbcr.jpg | Bin 0 -> 20228 bytes 3 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 imageio/imageio-jpeg/src/test/resources/jpeg/no-jfif-ycbcr.jpg diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index d56df77e..70ecac9b 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -66,6 +66,7 @@ import java.util.List; *

* Main features: *

    + *
  • Support for YCbCr JPEGs without JFIF segment (converted to RGB, using the embedded ICC profile if applicable)
  • *
  • Support for CMYK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable)
  • *
  • Support for Adobe YCCK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable)
  • *
  • Support for JPEGs containing ICC profiles with interpretation other than 'Perceptual' (profile is assumed to be 'Perceptual' and used)
  • @@ -298,26 +299,25 @@ public class JPEGImageReader extends ImageReaderBase { // } // } - // NOTE: We rely on the fact that unsupported images has no valid types. This is kind of hacky. - // Might want to look into the metadata, to see if there's a better way to identify these. - boolean unsupported = !delegate.getImageTypes(imageIndex).hasNext(); - ICC_Profile profile = getEmbeddedICCProfile(false); AdobeDCTSegment adobeDCT = getAdobeDCT(); + SOFSegment sof = getSOF(); + JPEGColorSpace sourceCSType = getSourceCSType(adobeDCT, sof); // We need to apply ICC profile unless the profile is sRGB/default gray (whatever that is) // - or only filter out the bad ICC profiles in the JPEGSegmentImageInputStream. if (delegate.canReadRaster() && ( - unsupported || + sourceCSType == JPEGColorSpace.CMYK || + sourceCSType == JPEGColorSpace.YCCK || adobeDCT != null && adobeDCT.getTransform() == AdobeDCTSegment.YCCK || - profile != null && !ColorSpaces.isCS_sRGB(profile))) { -// profile != null && (ColorSpaces.isOffendingColorProfile(profile) || profile.getColorSpaceType() == ColorSpace.TYPE_CMYK))) { + profile != null && !ColorSpaces.isCS_sRGB(profile)) || + sourceCSType == JPEGColorSpace.YCbCr && getRawImageType(imageIndex) != null) { // TODO: Issue warning? if (DEBUG) { System.out.println("Reading using raster and extra conversion"); System.out.println("ICC color profile: " + profile); } - return readImageAsRasterAndReplaceColorProfile(imageIndex, param, ensureDisplayProfile(profile)); + return readImageAsRasterAndReplaceColorProfile(imageIndex, param, sof, sourceCSType, adobeDCT, ensureDisplayProfile(profile)); } if (DEBUG) { @@ -327,14 +327,10 @@ public class JPEGImageReader extends ImageReaderBase { return delegate.read(imageIndex, param); } - private BufferedImage readImageAsRasterAndReplaceColorProfile(int imageIndex, ImageReadParam param, ICC_Profile profile) throws IOException { + private BufferedImage readImageAsRasterAndReplaceColorProfile(int imageIndex, ImageReadParam param, SOFSegment startOfFrame, JPEGColorSpace csType, AdobeDCTSegment adobeDCT, ICC_Profile profile) throws IOException { int origWidth = getWidth(imageIndex); int origHeight = getHeight(imageIndex); - AdobeDCTSegment adobeDCT = getAdobeDCT(); - SOFSegment startOfFrame = getSOF(); - JPEGColorSpace csType = getSourceCSType(adobeDCT, startOfFrame); - Iterator imageTypes = getImageTypes(imageIndex); BufferedImage image = getDestination(param, imageTypes, origWidth, origHeight); WritableRaster destination = image.getRaster(); diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java index 37a34f73..0a811122 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java @@ -600,6 +600,38 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase> 16) & 0xff, (expectedRGB[i] >> 16) & 0xff, 5); + assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5); + assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5); + } + } + // TODO: Test RGBA/YCbCrA handling @Test diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/no-jfif-ycbcr.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/no-jfif-ycbcr.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7e31ea76d4fb515d906804465d9fddb51149b1e5 GIT binary patch literal 20228 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 UO;!qWoy#iY-;8FHpybd0+0RxyNdN!< literal 0 HcmV?d00001 From cd197afc04849b69eead8c89c2937596f9a44d62 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 19 Sep 2013 09:52:47 +0200 Subject: [PATCH 22/98] TMI-META: Minor improvements in XMP parsing, PSD made public and faster dumping from JPEGSegmentUtil. --- .../imageio/metadata/jpeg/JPEGSegment.java | 2 +- .../metadata/jpeg/JPEGSegmentUtil.java | 71 +++++++++++-------- .../imageio/metadata/psd/PSD.java | 2 +- .../imageio/metadata/xmp/XMPReader.java | 49 ++++++++----- 4 files changed, 73 insertions(+), 51 deletions(-) diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java index 4de9bd34..d206f438 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java @@ -87,7 +87,7 @@ public final class JPEGSegment implements Serializable { return data != null ? data.length - offset() : 0; } - private int offset() { + int offset() { String identifier = identifier(); return identifier == null ? 0 : identifier.length() + 1; diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java index 10db0713..99d08d0a 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java @@ -33,6 +33,7 @@ import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; import com.twelvemonkeys.imageio.metadata.psd.PSDReader; import com.twelvemonkeys.imageio.metadata.xmp.XMP; import com.twelvemonkeys.imageio.metadata.xmp.XMPReader; +import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import javax.imageio.IIOException; import javax.imageio.ImageIO; @@ -245,38 +246,48 @@ public final class JPEGSegmentUtil { } public static void main(String[] args) throws IOException { - List segments = readSegments(ImageIO.createImageInputStream(new File(args[0])), ALL_SEGMENTS); - - for (JPEGSegment segment : segments) { - System.err.println("segment: " + segment); - - if ("Exif".equals(segment.identifier())) { - InputStream data = segment.data(); - //noinspection ResultOfMethodCallIgnored - data.read(); // Pad - - ImageInputStream stream = ImageIO.createImageInputStream(data); - - // Root entry is TIFF, that contains the EXIF sub-IFD - Directory tiff = new EXIFReader().read(stream); - System.err.println("EXIF: " + tiff); + for (String arg : args) { + if (args.length > 1) { + System.out.println("File: " + arg); + System.out.println("------"); } - else if (XMP.NS_XAP.equals(segment.identifier())) { - Directory xmp = new XMPReader().read(ImageIO.createImageInputStream(segment.data())); - System.err.println("XMP: " + xmp); + + List segments = readSegments(ImageIO.createImageInputStream(new File(arg)), ALL_SEGMENTS); + + for (JPEGSegment segment : segments) { + System.err.println("segment: " + segment); + + if ("Exif".equals(segment.identifier())) { + ImageInputStream stream = new ByteArrayImageInputStream(segment.data, segment.offset() + 1, segment.length() - 1); + + // Root entry is TIFF, that contains the EXIF sub-IFD + Directory tiff = new EXIFReader().read(stream); + System.err.println("EXIF: " + tiff); + } + else if (XMP.NS_XAP.equals(segment.identifier())) { + Directory xmp = new XMPReader().read(new ByteArrayImageInputStream(segment.data, segment.offset(), segment.length())); + System.err.println("XMP: " + xmp); + System.err.println(EXIFReader.HexDump.dump(segment.data)); + } + else if ("Photoshop 3.0".equals(segment.identifier())) { + // TODO: The "Photoshop 3.0" segment contains several image resources, of which one might contain + // IPTC metadata. Probably duplicated in the XMP though... + ImageInputStream stream = new ByteArrayImageInputStream(segment.data, segment.offset(), segment.length()); + Directory psd = new PSDReader().read(stream); + System.err.println("PSD: " + psd); + System.err.println(EXIFReader.HexDump.dump(segment.data)); + } + else if ("ICC_PROFILE".equals(segment.identifier())) { + // Skip + } + else { + System.err.println(EXIFReader.HexDump.dump(segment.data)); + } } - else if ("Photoshop 3.0".equals(segment.identifier())) { - // TODO: The "Photoshop 3.0" segment contains several image resources, of which one might contain - // IPTC metadata. Probably duplicated in the XMP though... - ImageInputStream stream = ImageIO.createImageInputStream(segment.data()); - Directory psd = new PSDReader().read(stream); - System.err.println("PSD: " + psd); - } - else if ("ICC_PROFILE".equals(segment.identifier())) { - // Skip - } - else { - System.err.println(EXIFReader.HexDump.dump(segment.data)); + + if (args.length > 1) { + System.out.println("------"); + System.out.println(); } } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSD.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSD.java index 4a1b95dc..94e8edac 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSD.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSD.java @@ -35,7 +35,7 @@ package com.twelvemonkeys.imageio.metadata.psd; * @author last modified by $Author: haraldk$ * @version $Id: PSD.java,v 1.0 24.01.12 16:51 haraldk Exp$ */ -interface PSD { +public interface PSD { static final int RESOURCE_TYPE = ('8' << 24) + ('B' << 16) + ('I' << 8) + 'M'; static final int RES_IPTC_NAA = 0x0404; diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java index e43d7557..642ecf7f 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPReader.java @@ -128,20 +128,8 @@ public final class XMPReader extends MetadataReader { Object value; - Node parseType = node.getAttributes().getNamedItemNS(XMP.NS_RDF, "parseType"); - if (parseType != null && "Resource".equals(parseType.getNodeValue())) { - // See: http://www.w3.org/TR/REC-rdf-syntax/#section-Syntax-parsetype-resource - List entries = new ArrayList(); - - for (Node child : asIterable(node.getChildNodes())) { - if (child.getNodeType() != Node.ELEMENT_NODE) { - continue; - } - - entries.add(new XMPEntry(child.getNamespaceURI() + child.getLocalName(), child.getLocalName(), getChildTextValue(child))); - } - - value = new RDFDescription(entries); + if (isResourceType(node)) { + value = parseAsResource(node); } else { // TODO: This method contains loads of duplication an should be cleaned up... @@ -178,6 +166,27 @@ public final class XMPReader extends MetadataReader { return new XMPDirectory(entries, toolkit); } + private boolean isResourceType(Node node) { + Node parseType = node.getAttributes().getNamedItemNS(XMP.NS_RDF, "parseType"); + + return parseType != null && "Resource".equals(parseType.getNodeValue()); + } + + private RDFDescription parseAsResource(Node node) { + // See: http://www.w3.org/TR/REC-rdf-syntax/#section-Syntax-parsetype-resource + List entries = new ArrayList(); + + for (Node child : asIterable(node.getChildNodes())) { + if (child.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + + entries.add(new XMPEntry(child.getNamespaceURI() + child.getLocalName(), child.getLocalName(), getChildTextValue(child))); + } + + return new RDFDescription(entries); + } + private void parseAttributesForKnownElements(Map> subdirs, Node desc) { // NOTE: NamedNodeMap does not have any particular order... NamedNodeMap attributes = desc.getAttributes(); @@ -201,15 +210,13 @@ public final class XMPReader extends MetadataReader { private Object getChildTextValue(final Node node) { for (Node child : asIterable(node.getChildNodes())) { if (XMP.NS_RDF.equals(child.getNamespaceURI()) && "Alt".equals(child.getLocalName())) { - // Support for -> return a Map (keyed on xml:lang?) + // Support for -> return a Map keyed on xml:lang Map alternatives = new LinkedHashMap(); for (Node alternative : asIterable(child.getChildNodes())) { if (XMP.NS_RDF.equals(alternative.getNamespaceURI()) && "li".equals(alternative.getLocalName())) { - //return getChildTextValue(alternative); NamedNodeMap attributes = alternative.getAttributes(); Node key = attributes.getNamedItem("xml:lang"); - - alternatives.put(key.getTextContent(), getChildTextValue(alternative)); + alternatives.put(key == null ? null : key.getTextContent(), getChildTextValue(alternative)); } } @@ -235,9 +242,13 @@ public final class XMPReader extends MetadataReader { } } + // Need to support rdf:parseType="Resource" here as well... + if (isResourceType(node)) { + return parseAsResource(node); + } + Node child = node.getFirstChild(); String strVal = child != null ? child.getNodeValue() : null; - return strVal != null ? strVal.trim() : ""; } From 1acc04eeaf4ab037ef6fd3b3b707676933004ac3 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Fri, 27 Sep 2013 14:21:31 +0200 Subject: [PATCH 23/98] TMC-IOENC-11: Fixed problem introduced when migrating byte[] -> ByteBuffer --- .../com/twelvemonkeys/io/enc/Encoder.java | 6 ++-- .../twelvemonkeys/io/enc/EncoderStream.java | 5 +++- .../twelvemonkeys/io/enc/PackBitsEncoder.java | 28 +++++++++++-------- .../io/enc/EncoderAbstractTestCase.java | 11 +++++++- .../io/enc/PackBitsEncoderTestCase.java | 5 ---- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Encoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Encoder.java index c1a126b0..1864e4dc 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Encoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Encoder.java @@ -33,9 +33,9 @@ import java.io.OutputStream; import java.nio.ByteBuffer; /** - * Interface for endcoders. + * Interface for encoders. * An {@code Encoder} may be used with an {@code EncoderStream}, to perform - * on-the-fly enoding to an {@code OutputStream}. + * on-the-fly encoding to an {@code OutputStream}. *

    * Important note: Encoder implementations are typically not synchronized. * @@ -48,7 +48,7 @@ import java.nio.ByteBuffer; public interface Encoder { /** - * Encodes up to {@code pBuffer.length} bytes into the given input stream, + * Encodes up to {@code buffer.remaining()} bytes into the given input stream, * from the given buffer. * * @param stream the output stream to encode data to diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java index 6cf9ee31..f1ba67ef 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java @@ -44,6 +44,7 @@ import java.nio.ByteBuffer; * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java#2 $ */ public final class EncoderStream extends FilterOutputStream { + // TODO: This class need a test case ASAP!!! protected final Encoder encoder; private final boolean flushOnWrite; @@ -91,7 +92,9 @@ public final class EncoderStream extends FilterOutputStream { } private void encodeBuffer() throws IOException { - if (buffer.hasRemaining()) { + if (buffer.position() != 0) { + buffer.flip(); + // Make sure all remaining data in buffer is written to the stream encoder.encode(out, buffer); diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java index 61edf7bb..c11ccd97 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java @@ -73,53 +73,57 @@ public final class PackBitsEncoder implements Encoder { } public void encode(final OutputStream stream, final ByteBuffer buffer) throws IOException { + encode(stream, buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + buffer.position(buffer.remaining()); + } + + private void encode(OutputStream pStream, byte[] pBuffer, int pOffset, int pLength) throws IOException { // NOTE: It's best to encode a 2 byte repeat // run as a replicate run except when preceded and followed by a // literal run, in which case it's best to merge the three into one // literal run. Always encode 3 byte repeats as replicate runs. // NOTE: Worst case: output = input + (input + 127) / 128 - int offset = buffer.position(); - final int max = buffer.remaining() - 1; + int offset = pOffset; + final int max = pOffset + pLength - 1; final int maxMinus1 = max - 1; - final byte[] pBuffer = buffer.array(); while (offset <= max) { // Compressed run int run = 1; byte replicate = pBuffer[offset]; - while (run < 127 && offset < max && pBuffer[offset] == pBuffer[offset + 1]) { + while(run < 127 && offset < max && pBuffer[offset] == pBuffer[offset + 1]) { offset++; run++; } if (run > 1) { offset++; - stream.write(-(run - 1)); - stream.write(replicate); + pStream.write(-(run - 1)); + pStream.write(replicate); } // Literal run run = 0; while ((run < 128 && ((offset < max && pBuffer[offset] != pBuffer[offset + 1]) || (offset < maxMinus1 && pBuffer[offset] != pBuffer[offset + 2])))) { - this.buffer[run++] = pBuffer[offset++]; + buffer[run++] = pBuffer[offset++]; } // If last byte, include it in literal run, if space if (offset == max && run > 0 && run < 128) { - this.buffer[run++] = pBuffer[offset++]; + buffer[run++] = pBuffer[offset++]; } if (run > 0) { - stream.write(run - 1); - stream.write(this.buffer, 0, run); + pStream.write(run - 1); + pStream.write(buffer, 0, run); } // If last byte, and not space, start new literal run if (offset == max && (run <= 0 || run >= 128)) { - stream.write(0); - stream.write(pBuffer[offset++]); + pStream.write(0); + pStream.write(pBuffer[offset++]); } } } diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java index 0977a654..6376a73d 100644 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java @@ -54,7 +54,12 @@ public abstract class EncoderAbstractTestCase extends ObjectAbstractTestCase { OutputStream out = new EncoderStream(outBytes, createEncoder(), true); try { - out.write(data); + // Provoke failure for encoders that doesn't take array offset properly into account + int off = (data.length + 1) / 2; + out.write(data, 0, off); + if (data.length > off) { + out.write(data, off, data.length - off); + } } finally { out.close(); @@ -127,4 +132,8 @@ public abstract class EncoderAbstractTestCase extends ObjectAbstractTestCase { } } } + + // TODO: Test that the transition from byte[] to ByteBuffer didn't introduce bugs when writing to a wrapped array with offset. + + } diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/PackBitsEncoderTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/PackBitsEncoderTestCase.java index 023e99e1..d214fd3f 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/PackBitsEncoderTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/PackBitsEncoderTestCase.java @@ -1,10 +1,5 @@ package com.twelvemonkeys.io.enc; -import com.twelvemonkeys.io.enc.Decoder; -import com.twelvemonkeys.io.enc.Encoder; -import com.twelvemonkeys.io.enc.PackBitsDecoder; -import com.twelvemonkeys.io.enc.PackBitsEncoder; - /** * PackBitsEncoderTest *

    From c8061eb0c406388f5860c3d6dee42d932bf926a5 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sat, 28 Sep 2013 12:32:01 +0200 Subject: [PATCH 24/98] TMI-JPEG: Regression fix for NPE in metadata if delegate returns null metadata. --- .../imageio/plugins/jpeg/JPEGImageReader.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 70ecac9b..2f33ac12 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -1006,9 +1006,10 @@ public class JPEGImageReader extends ImageReaderBase { public IIOMetadata getImageMetadata(int imageIndex) throws IOException { IIOMetadata metadata = delegate.getImageMetadata(imageIndex); - String format = metadata.getNativeMetadataFormatName(); - IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(format); - Node jpegVariety = tree.getElementsByTagName("JPEGvariety").item(0); + if (metadata != null) { + String format = metadata.getNativeMetadataFormatName(); + IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(format); + Node jpegVariety = tree.getElementsByTagName("JPEGvariety").item(0); // TODO: Allow EXIF (as app1EXIF) in the JPEGvariety (sic) node. // As EXIF is (a subset of) TIFF, (and the EXIF data is a valid TIFF stream) probably use something like: @@ -1030,16 +1031,17 @@ public class JPEGImageReader extends ImageReaderBase { the version to the method/constructor used to obtain an IIOMetadata object.) */ - IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC"); - app2ICC.setUserObject(getEmbeddedICCProfile(true)); - Node jpegVarietyFirstChild = jpegVariety.getFirstChild(); - if (jpegVarietyFirstChild != null) { - jpegVarietyFirstChild.appendChild(app2ICC); - } + IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC"); + app2ICC.setUserObject(getEmbeddedICCProfile(true)); + Node jpegVarietyFirstChild = jpegVariety.getFirstChild(); + if (jpegVarietyFirstChild != null) { + jpegVarietyFirstChild.appendChild(app2ICC); + } // new XMLSerializer(System.err, System.getProperty("file.encoding")).serialize(tree, false); - metadata.mergeTree(format, tree); + metadata.mergeTree(format, tree); + } return metadata; } From b14363da3b7de65258223ee37babf55d3c974d4b Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Mon, 21 Oct 2013 19:31:04 +0200 Subject: [PATCH 25/98] TMI-JPEG-4: Now does a pretty decent job at glossing over metadata issues. --- .../plugins/jpeg/EXIFThumbnailReader.java | 15 +- .../plugins/jpeg/JFXXThumbnailReader.java | 31 +- .../imageio/plugins/jpeg/JPEGImageReader.java | 290 +++++++++++++++-- .../jpeg/JPEGSegmentImageInputStream.java | 3 +- .../imageio/plugins/jpeg/ThumbnailReader.java | 35 ++- .../plugins/jpeg/EXIFThumbnailReaderTest.java | 2 +- .../plugins/jpeg/JFXXThumbnailReaderTest.java | 3 +- .../plugins/jpeg/JPEGImageReaderTest.java | 292 +++++++++++++++++- .../jpeg/JPEGSegmentImageInputStreamTest.java | 34 +- 9 files changed, 634 insertions(+), 71 deletions(-) diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java index a54645ae..828056f9 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java @@ -32,9 +32,12 @@ import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; import com.twelvemonkeys.imageio.metadata.exif.TIFF; import com.twelvemonkeys.imageio.util.IIOUtil; +import com.twelvemonkeys.lang.Validate; import javax.imageio.IIOException; +import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.MemoryCacheImageInputStream; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -51,14 +54,16 @@ import java.util.Arrays; * @version $Id: EXIFThumbnail.java,v 1.0 18.04.12 12:19 haraldk Exp$ */ final class EXIFThumbnailReader extends ThumbnailReader { + private final ImageReader reader; private final Directory ifd; private final ImageInputStream stream; private final int compression; private transient SoftReference cachedThumbnail; - public EXIFThumbnailReader(ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, Directory ifd, ImageInputStream stream) { + public EXIFThumbnailReader(ThumbnailReadProgressListener progressListener, ImageReader jpegReader, int imageIndex, int thumbnailIndex, Directory ifd, ImageInputStream stream) { super(progressListener, imageIndex, thumbnailIndex); + this.reader = Validate.notNull(jpegReader); this.ifd = ifd; this.stream = stream; @@ -126,7 +131,13 @@ final class EXIFThumbnailReader extends ThumbnailReader { input = new SequenceInputStream(new ByteArrayInputStream(fakeEmptyExif), input); try { - return readJPEGThumbnail(input); + MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(input); + try { + return readJPEGThumbnail(reader, stream); + } + finally { + stream.close(); + } } finally { input.close(); diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java index cce8b3bb..8de14dcb 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java @@ -29,10 +29,14 @@ package com.twelvemonkeys.imageio.plugins.jpeg; import com.twelvemonkeys.image.InverseColorMapIndexColorModel; +import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; +import com.twelvemonkeys.lang.Validate; import javax.imageio.IIOException; +import javax.imageio.ImageReader; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.stream.ImageInputStream; import java.awt.image.*; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.lang.ref.SoftReference; @@ -45,12 +49,14 @@ import java.lang.ref.SoftReference; */ final class JFXXThumbnailReader extends ThumbnailReader { + private final ImageReader reader; private final JFXXSegment segment; private transient SoftReference cachedThumbnail; - protected JFXXThumbnailReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex, final JFXXSegment segment) { + protected JFXXThumbnailReader(final ThumbnailReadProgressListener progressListener, ImageReader jpegReader, final int imageIndex, final int thumbnailIndex, final JFXXSegment segment) { super(progressListener, imageIndex, thumbnailIndex); + this.reader = Validate.notNull(jpegReader); this.segment = segment; } @@ -79,11 +85,30 @@ final class JFXXThumbnailReader extends ThumbnailReader { return thumbnail; } + public IIOMetadata readMetadata() throws IOException { + ImageInputStream input = new ByteArrayImageInputStream(segment.thumbnail); + + try { + reader.setInput(input); + + return reader.getImageMetadata(0); + } + finally { + input.close(); + } + } + private BufferedImage readJPEGCached(boolean pixelsExposed) throws IOException { BufferedImage thumbnail = cachedThumbnail != null ? cachedThumbnail.get() : null; if (thumbnail == null) { - thumbnail = readJPEGThumbnail(new ByteArrayInputStream(segment.thumbnail)); + ImageInputStream stream = new ByteArrayImageInputStream(segment.thumbnail); + try { + thumbnail = readJPEGThumbnail(reader, stream); + } + finally { + stream.close(); + } } cachedThumbnail = pixelsExposed ? null : new SoftReference(thumbnail); diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 2f33ac12..210975cb 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -41,11 +41,15 @@ import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment; import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; import com.twelvemonkeys.imageio.util.ProgressListenerBase; import com.twelvemonkeys.lang.Validate; +import com.twelvemonkeys.xml.XMLSerializer; +import org.w3c.dom.Element; import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import javax.imageio.*; import javax.imageio.event.IIOReadUpdateListener; import javax.imageio.event.IIOReadWarningListener; +import javax.imageio.metadata.IIOInvalidTreeException; import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.spi.ImageReaderSpi; @@ -57,6 +61,7 @@ import java.awt.color.ICC_ColorSpace; import java.awt.color.ICC_Profile; import java.awt.image.*; import java.io.*; +import java.nio.charset.Charset; import java.util.*; import java.util.List; @@ -97,12 +102,16 @@ public class JPEGImageReader extends ImageReaderBase { private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug")); + /** Internal constant for referring all APP segments */ + private static final int ALL_APP_MARKERS = -1; + /** Segment identifiers for the JPEG segments we care about reading. */ private static final Map> SEGMENT_IDENTIFIERS = createSegmentIds(); private static Map> createSegmentIds() { Map> map = new LinkedHashMap>(); + /* // JFIF/JFXX APP0 markers map.put(JPEG.APP0, JPEGSegmentUtil.ALL_IDS); @@ -114,6 +123,12 @@ public class JPEGImageReader extends ImageReaderBase { // Adobe APP14 marker map.put(JPEG.APP14, Collections.singletonList("Adobe")); + //*/ + + // Need all APP markers to be able to re-generate proper metadata later + for (int appMarker = JPEG.APP0; appMarker <= JPEG.APP15; appMarker++) { + map.put(appMarker, JPEGSegmentUtil.ALL_IDS); + } // SOFn markers map.put(JPEG.SOF0, null); @@ -135,6 +150,7 @@ public class JPEGImageReader extends ImageReaderBase { /** Our JPEG reading delegate */ private final ImageReader delegate; + private ImageReader thumbnailReader; /** Listens to progress updates in the delegate, and delegates back to this instance */ private final ProgressDelegator progressDelegator; @@ -163,6 +179,10 @@ public class JPEGImageReader extends ImageReaderBase { segments = null; thumbnails = null; + if (thumbnailReader != null) { + thumbnailReader.reset(); + } + installListeners(); } @@ -170,6 +190,11 @@ public class JPEGImageReader extends ImageReaderBase { public void dispose() { super.dispose(); + if (thumbnailReader != null) { + thumbnailReader.dispose(); + thumbnailReader = null; + } + delegate.dispose(); } @@ -317,6 +342,7 @@ public class JPEGImageReader extends ImageReaderBase { System.out.println("ICC color profile: " + profile); } + // TODO: Possible to optimize slightly, to avoid readAsRaster for non-CMyK and other good types? return readImageAsRasterAndReplaceColorProfile(imageIndex, param, sof, sourceCSType, adobeDCT, ensureDisplayProfile(profile)); } @@ -673,7 +699,8 @@ public class JPEGImageReader extends ImageReaderBase { List appSegments = Collections.emptyList(); for (JPEGSegment segment : segments) { - if (segment.marker() == marker && (identifier == null || identifier.equals(segment.identifier()))) { + if ((marker == ALL_APP_MARKERS && segment.marker() >= JPEG.APP0 && segment.marker() <= JPEG.APP15 || segment.marker() == marker) + && (identifier == null || identifier.equals(segment.identifier()))) { if (appSegments == Collections.EMPTY_LIST) { appSegments = new ArrayList(segments.size()); } @@ -928,7 +955,7 @@ public class JPEGImageReader extends ImageReaderBase { case JFXXSegment.JPEG: case JFXXSegment.INDEXED: case JFXXSegment.RGB: - thumbnails.add(new JFXXThumbnailReader(thumbnailProgressDelegator, imageIndex, thumbnails.size(), jfxx)); + thumbnails.add(new JFXXThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), imageIndex, thumbnails.size(), jfxx)); break; default: processWarningOccurred("Unknown JFXX extension code: " + jfxx.extensionCode); @@ -956,7 +983,7 @@ public class JPEGImageReader extends ImageReaderBase { // 1 = no compression, 6 = JPEG compression (default) if (compression == null || compression.getValue().equals(1) || compression.getValue().equals(6)) { - thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, 0, thumbnails.size(), ifd1, stream)); + thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), 0, thumbnails.size(), ifd1, stream)); } else { processWarningOccurred("EXIF IFD with unknown compression (expected 1 or 6): " + compression.getValue()); @@ -967,6 +994,14 @@ public class JPEGImageReader extends ImageReaderBase { } } + private ImageReader getThumbnailReader() throws IOException { + if (thumbnailReader == null) { + thumbnailReader = delegate.getOriginatingProvider().createReaderInstance(); + } + + return thumbnailReader; + } + @Override public int getNumThumbnails(final int imageIndex) throws IOException { readThumbnailMetadata(imageIndex); @@ -1004,43 +1039,240 @@ public class JPEGImageReader extends ImageReaderBase { @Override public IIOMetadata getImageMetadata(int imageIndex) throws IOException { + // TODO: Extract metadata handling in separate class, for less mess and easier testing + // We filter out pretty much everything from the stream.. + // Meaning we have to read *all APP segments* and re-insert into metadata. + + // TODO: There's a bug in the merging code in JPEGMetadata mergeUnknownNode that makes sure all "unknown" nodes are added twice in certain conditions.... ARGHBL... + // TODO: 1: Work around + // TODO: 2: REPORT BUG! + + List appSegments = getAppSegments(ALL_APP_MARKERS, null); +// System.out.println("appSegments: " + appSegments); + IIOMetadata metadata = delegate.getImageMetadata(imageIndex); if (metadata != null) { String format = metadata.getNativeMetadataFormatName(); IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(format); - Node jpegVariety = tree.getElementsByTagName("JPEGvariety").item(0); + IIOMetadataNode jpegVariety = (IIOMetadataNode) tree.getElementsByTagName("JPEGvariety").item(0); + IIOMetadataNode markerSequence = (IIOMetadataNode) tree.getElementsByTagName("markerSequence").item(0); - // TODO: Allow EXIF (as app1EXIF) in the JPEGvariety (sic) node. - // As EXIF is (a subset of) TIFF, (and the EXIF data is a valid TIFF stream) probably use something like: - // http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata - /* - from: http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html + JFIFSegment jfifSegment = getJFIF(); + JFXXSegment jfxxSegment = getJFXX(); + AdobeDCTSegment adobeDCT = getAdobeDCT(); + ICC_Profile embeddedICCProfile = getEmbeddedICCProfile(true); + SOFSegment sof = getSOF(); - In future versions of the JPEG metadata format, other varieties of JPEG metadata may be supported (e.g. Exif) - by defining other types of nodes which may appear as a child of the JPEGvariety node. + boolean hasRealJFIF = false; + boolean hasRealJFXX = false; + boolean hasRealICC = false; - (Note that an application wishing to interpret Exif metadata given a metadata tree structure in the - javax_imageio_jpeg_image_1.0 format must check for an unknown marker segment with a tag indicating an - APP1 marker and containing data identifying it as an Exif marker segment. Then it may use application-specific - code to interpret the data in the marker segment. If such an application were to encounter a metadata tree - formatted according to a future version of the JPEG metadata format, the Exif marker segment might not be - unknown in that format - it might be structured as a child node of the JPEGvariety node. + if (jfifSegment != null) { + // Normal case, conformant JFIF with 1 or 3 components + // TODO: Test if we have CMY or other bad color space? + // TODO: Remove JFIF if app14Adobe transform is YCCK (and isn't incorrect...) + if (sof.componentsInFrame() == 1 || sof.componentsInFrame() == 3) { + IIOMetadataNode jfif = new IIOMetadataNode("app0JFIF"); + jfif.setAttribute("majorVersion", String.valueOf(jfifSegment.majorVersion)); + jfif.setAttribute("minorVersion", String.valueOf(jfifSegment.minorVersion)); + jfif.setAttribute("resUnits", String.valueOf(jfifSegment.units)); + jfif.setAttribute("Xdensity", String.valueOf(jfifSegment.xDensity)); + jfif.setAttribute("Ydensity", String.valueOf(jfifSegment.yDensity)); + jfif.setAttribute("thumbWidth", String.valueOf(jfifSegment.xThumbnail)); + jfif.setAttribute("thumbHeight", String.valueOf(jfifSegment.yThumbnail)); - Thus, it is important for an application to specify which version to use by passing the string identifying - the version to the method/constructor used to obtain an IIOMetadata object.) - */ + jpegVariety.appendChild(jfif); + hasRealJFIF = true; - IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC"); - app2ICC.setUserObject(getEmbeddedICCProfile(true)); - Node jpegVarietyFirstChild = jpegVariety.getFirstChild(); - if (jpegVarietyFirstChild != null) { - jpegVarietyFirstChild.appendChild(app2ICC); + // Add app2ICC and JFXX as proper nodes + if (embeddedICCProfile != null) { + IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC"); + app2ICC.setUserObject(embeddedICCProfile); + jfif.appendChild(app2ICC); + hasRealICC = true; + } + + if (jfxxSegment != null) { + IIOMetadataNode JFXX = new IIOMetadataNode("JFXX"); + jfif.appendChild(JFXX); + IIOMetadataNode app0JFXX = new IIOMetadataNode("app0JFXX"); + app0JFXX.setAttribute("extensionCode", String.valueOf(jfxxSegment.extensionCode)); + + JFXXThumbnailReader reader = new JFXXThumbnailReader(null, getThumbnailReader(), imageIndex, -1, jfxxSegment); + + IIOMetadataNode jfifThumb; + switch (jfxxSegment.extensionCode) { + case JFXXSegment.JPEG: + jfifThumb = new IIOMetadataNode("JFIFthumbJPEG"); + // Contains it's own "markerSequence" with full DHT, DQT, SOF etc... + IIOMetadata thumbMeta = reader.readMetadata(); + Node thumbTree = thumbMeta.getAsTree(format); + jfifThumb.appendChild(thumbTree.getLastChild()); + app0JFXX.appendChild(jfifThumb); + break; + + case JFXXSegment.INDEXED: + jfifThumb = new IIOMetadataNode("JFIFthumbPalette"); + jfifThumb.setAttribute("thumbWidth", String.valueOf(reader.getWidth())); + jfifThumb.setAttribute("thumbHeight", String.valueOf(reader.getHeight())); + app0JFXX.appendChild(jfifThumb); + break; + + case JFXXSegment.RGB: + jfifThumb = new IIOMetadataNode("JFIFthumbRGB"); + jfifThumb.setAttribute("thumbWidth", String.valueOf(reader.getWidth())); + jfifThumb.setAttribute("thumbHeight", String.valueOf(reader.getHeight())); + app0JFXX.appendChild(jfifThumb); + break; + + default: + processWarningOccurred(String.format("Unknown JFXX extension code: %d", jfxxSegment.extensionCode)); + } + + JFXX.appendChild(app0JFXX); + hasRealJFXX = true; + } + } + else { + // Typically CMYK with JFIF segment (Adobe or similar). + processWarningOccurred(String.format( + "Incompatible JFIF marker segment in stream. " + + "SOF%d has %d color components, JFIF allows only 1 or 3 components. Ignoring JFIF marker.", + sof.marker & 0xf, sof.componentsInFrame() + )); + } } - // new XMLSerializer(System.err, System.getProperty("file.encoding")).serialize(tree, false); + // Special case: Broken AdobeDCT segment, inconsistent with SOF, use values from SOF + if (adobeDCT != null && adobeDCT.getTransform() == AdobeDCTSegment.YCCK && sof.componentsInFrame() < 4) { + processWarningOccurred(String.format( + "Invalid Adobe App14 marker. Indicates YCCK/CMYK data, but SOF%d has %d color components. " + + "Ignoring Adobe App14 marker.", + sof.marker & 0xf, sof.componentsInFrame() + )); - metadata.mergeTree(format, tree); + // Remove bad AdobeDCT + NodeList app14Adobe = tree.getElementsByTagName("app14Adobe"); + for (int i = app14Adobe.getLength() - 1; i >= 0; i--) { + Node item = app14Adobe.item(i); + item.getParentNode().removeChild(item); + } + + + // TODO: Add app14 as "unknown" marker? +// IIOMetadataNode app14Adobe = new IIOMetadataNode("app14Adobe"); +// app14Adobe.setAttribute("version", String.valueOf(adobeDCT.getVersion())); +// app14Adobe.setAttribute("flags0", String.valueOf(adobeDCT.getFlags0())); +// app14Adobe.setAttribute("flags1", String.valueOf(adobeDCT.getFlags1())); +// app14Adobe.setAttribute("transform", String.valueOf(sof.componentsInFrame() == 3 ? )); +// markerSequence.appendChild(app14Adobe); + } + + Node next = null; + for (JPEGSegment segment : appSegments) { + // TODO: Except real app0JFIF, app0JFXX, app2ICC and app14Adobe, add all the app segments that we filtered away as "unknown" markers + if (segment.marker() == JPEG.APP0 && "JFIF".equals(segment.identifier()) && hasRealJFIF) { + continue; + } + else if (segment.marker() == JPEG.APP0 && "JFXX".equals(segment.identifier()) && hasRealJFXX) { + continue; + } + else if (segment.marker() == JPEG.APP1 && "Exif".equals(segment.identifier()) /* always inserted */) { + continue; + } + else if (segment.marker() == JPEG.APP2 && "ICC_PROFILE".equals(segment.identifier()) && hasRealICC) { + continue; + } + else if (segment.marker() == JPEG.APP14 && "Adobe".equals(segment.identifier()) /* always inserted */) { + continue; + } + + IIOMetadataNode unknown = new IIOMetadataNode("unknown"); + unknown.setAttribute("MarkerTag", Integer.toString(segment.marker() & 0xff)); + + DataInputStream stream = new DataInputStream(segment.data()); + + try { + String identifier = segment.identifier(); + int off = identifier != null ? identifier.length() + 1 : 0; + + byte[] data = new byte[off + segment.length()]; + + if (identifier != null) { + System.arraycopy(identifier.getBytes(Charset.forName("ASCII")), 0, data, 0, identifier.length()); + } + + stream.readFully(data, off, segment.length()); + + unknown.setUserObject(data); + } + finally { + stream.close(); + } + + if (next == null) { + next = markerSequence.getFirstChild(); + } + + markerSequence.insertBefore(unknown, next); + } + + // Inconsistency issue in the com.sun classes, it can read metadata with dht containing + // more than 4 children, but will not allow setting such a tree... + // We'll split AC/DC tables into separate dht nodes. + NodeList dhts = markerSequence.getElementsByTagName("dht"); + for (int j = 0; j < dhts.getLength(); j++) { + Node dht = dhts.item(j); + NodeList dhtables = dht.getChildNodes(); + + if (dhtables.getLength() > 4) { + IIOMetadataNode acTables = new IIOMetadataNode("dht"); + dht.getParentNode().insertBefore(acTables, dht.getNextSibling()); + + + // Split into 2 dht nodes, one for AC and one for DC + for (int i = 0; i < dhtables.getLength(); i++) { + Element dhtable = (Element) dhtables.item(i); + String tableClass = dhtable.getAttribute("class"); + if ("1".equals(tableClass)) { + dht.removeChild(dhtable); + acTables.appendChild(dhtable); + } + } + } + } + + // TODO: Allow EXIF (as app1EXIF) in the JPEGvariety (sic) node. + // As EXIF is (a subset of) TIFF, (and the EXIF data is a valid TIFF stream) probably use something like: + // http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata + /* + from: http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html + + In future versions of the JPEG metadata format, other varieties of JPEG metadata may be supported (e.g. Exif) + by defining other types of nodes which may appear as a child of the JPEGvariety node. + + (Note that an application wishing to interpret Exif metadata given a metadata tree structure in the + javax_imageio_jpeg_image_1.0 format must check for an unknown marker segment with a tag indicating an + APP1 marker and containing data identifying it as an Exif marker segment. Then it may use application-specific + code to interpret the data in the marker segment. If such an application were to encounter a metadata tree + formatted according to a future version of the JPEG metadata format, the Exif marker segment might not be + unknown in that format - it might be structured as a child node of the JPEGvariety node. + + Thus, it is important for an application to specify which version to use by passing the string identifying + the version to the method/constructor used to obtain an IIOMetadata object.) + */ + +// new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(tree, false); + + try { + metadata.setFromTree(format, tree); + } + catch (IIOInvalidTreeException e) { + new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(tree, false); + throw e; + } +// metadata.mergeTree(format, tree); // TODO: Merging does not work, as the "unknown" tags duplicate insert bug ruins everything. Try set instead... } return metadata; @@ -1379,9 +1611,11 @@ public class JPEGImageReader extends ImageReaderBase { // System.err.println("thumbnail: " + thumbnail); showIt(thumbnail, String.format("Thumbnail: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight())); } + + reader.getImageMetadata(0); } catch (IIOException e) { - System.err.println("Could not read thumbnails: " + e.getMessage()); + System.err.println("Could not read thumbnails: " + arg + ": " + e.getMessage()); e.printStackTrace(); } } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java index 23ab8f8b..77b7c10b 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java @@ -97,8 +97,9 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl { marker = 0xff00 | stream.readUnsignedByte(); } + // TODO: Optionally skip JFIF only for non-JFIF conformant streams // TODO: Refactor to make various segments optional, we probably only want the "Adobe" APP14 segment, 'Exif' APP1 and very few others - if (isAppSegmentMarker(marker) && marker != JPEG.APP0 && !(marker == JPEG.APP1 && isAppSegmentWithId("Exif", stream)) && marker != JPEG.APP14) { + if (isAppSegmentMarker(marker) && !(marker == JPEG.APP1 && isAppSegmentWithId("Exif", stream)) && marker != JPEG.APP14) { int length = stream.readUnsignedShort(); // Length including length field itself stream.seek(realPosition + 2 + length); // Skip marker (2) + length } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java index 6354ef93..370022c6 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java @@ -28,12 +28,12 @@ package com.twelvemonkeys.imageio.plugins.jpeg; -import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; import java.awt.*; import java.awt.color.ColorSpace; import java.awt.image.*; import java.io.IOException; -import java.io.InputStream; /** * ThumbnailReader @@ -42,6 +42,7 @@ import java.io.InputStream; * @author last modified by $Author: haraldk$ * @version $Id: ThumbnailReader.java,v 1.0 18.04.12 12:22 haraldk Exp$ */ +// TODO: Get rid of the com.sun import!! abstract class ThumbnailReader { private final ThumbnailReadProgressListener progressListener; @@ -49,10 +50,11 @@ abstract class ThumbnailReader { protected final int thumbnailIndex; protected ThumbnailReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex) { - this.progressListener = progressListener; + this.progressListener = progressListener != null ? progressListener : new NullProgressListener(); this.imageIndex = imageIndex; this.thumbnailIndex = thumbnailIndex; } + protected final void processThumbnailStarted() { progressListener.processThumbnailStarted(imageIndex, thumbnailIndex); } @@ -65,8 +67,20 @@ abstract class ThumbnailReader { progressListener.processThumbnailComplete(); } - static protected BufferedImage readJPEGThumbnail(InputStream stream) throws IOException { - return ImageIO.read(stream); + static protected BufferedImage readJPEGThumbnail(final ImageReader reader, final ImageInputStream stream) throws IOException { +// try { +// try { + reader.setInput(stream); + + return reader.read(0); +// } +// finally { +// input.close(); +// } +// } +// finally { +// reader.dispose(); +// } } static protected BufferedImage readRawThumbnail(final byte[] thumbnail, final int size, final int offset, int w, int h) { @@ -82,4 +96,15 @@ abstract class ThumbnailReader { public abstract int getWidth() throws IOException; public abstract int getHeight() throws IOException; + + private static class NullProgressListener implements ThumbnailReadProgressListener { + public void processThumbnailStarted(int imageIndex, int thumbnailIndex) { + } + + public void processThumbnailProgress(float percentageDone) { + } + + public void processThumbnailComplete() { + } + } } diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java index f1b92e27..9a13a17f 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java @@ -74,7 +74,7 @@ public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest { assertEquals(2, ifds.directoryCount()); - return new EXIFThumbnailReader(progressListener, imageIndex, thumbnailIndex, ifds.getDirectory(1), exifStream); + return new EXIFThumbnailReader(progressListener, ImageIO.getImageReadersByFormatName("JPEG").next(), imageIndex, thumbnailIndex, ifds.getDirectory(1), exifStream); } @Test diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java index 4f8db3ff..0b8a6402 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java @@ -34,6 +34,7 @@ import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; import org.junit.Test; import org.mockito.InOrder; +import javax.imageio.ImageIO; import javax.imageio.stream.ImageInputStream; import java.awt.image.BufferedImage; import java.io.IOException; @@ -63,7 +64,7 @@ public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest { assertFalse(segments.isEmpty()); JPEGSegment jfxx = segments.get(0); - return new JFXXThumbnailReader(progressListener, imageIndex, thumbnailIndex, JFXXSegment.read(jfxx.data(), jfxx.length())); + return new JFXXThumbnailReader(progressListener, ImageIO.getImageReadersByFormatName("jpeg").next(), imageIndex, thumbnailIndex, JFXXSegment.read(jfxx.data(), jfxx.length())); } @Test diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java index 0a811122..6666d9ac 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java @@ -29,26 +29,33 @@ package com.twelvemonkeys.imageio.plugins.jpeg; import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import org.hamcrest.core.IsInstanceOf; import org.junit.Test; +import org.mockito.internal.matchers.GreaterThan; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; -import javax.imageio.IIOException; -import javax.imageio.ImageIO; -import javax.imageio.ImageReadParam; -import javax.imageio.ImageTypeSpecifier; +import javax.imageio.*; import javax.imageio.event.IIOReadWarningListener; import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.plugins.jpeg.JPEGHuffmanTable; +import javax.imageio.plugins.jpeg.JPEGQTable; import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; import java.awt.*; import java.awt.color.ColorSpace; +import java.awt.color.ICC_Profile; import java.awt.image.BufferedImage; import java.awt.image.DataBufferByte; import java.io.IOException; -import java.util.Arrays; -import java.util.Iterator; +import java.util.*; import java.util.List; import static org.junit.Assert.*; +import static org.junit.Assert.assertArrayEquals; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; @@ -627,15 +634,15 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase> 16) & 0xff, (expectedRGB[i] >> 16) & 0xff, 5); - assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5); - assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5); + assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5); + assertEquals((actualRGB ) & 0xff, (expectedRGB[i] ) & 0xff, 5); } } // TODO: Test RGBA/YCbCrA handling @Test - public void testReadMetadataMaybeNull() throws IOException { + public void testReadMetadata() throws IOException { // Just test that we can read the metadata without exceptions JPEGImageReader reader = createReader(); @@ -646,11 +653,276 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase(0)); + + NodeList unknowns = markerSequence.getElementsByTagName("unknown"); + for (int j = 0; j < unknowns.getLength(); j++) { + IIOMetadataNode unknown = (IIOMetadataNode) unknowns.item(j); + assertNotNull(unknown.getUserObject()); // All unknowns must have user object (data array) + } } catch (IIOException e) { - System.err.println(String.format("WARNING: Reading metadata failed for %s image %s: %s", testData, i, e.getMessage())); + fail(String.format("Reading metadata failed for %s image %s: %s", testData, i, e.getMessage())); } } } } + + @Test + public void testReadInconsistentMetadata() throws IOException { + // A collection of JPEG files that makes the JPEGImageReader throw exception "Inconsistent metadata read from stream"... + List resources = Arrays.asList( + "/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg", // Ok + "/jpeg/gray-sample.jpg", // Ok + "/jpeg/cmyk-sample.jpg", + "/jpeg/cmyk-sample-multiple-chunk-icc.jpg", + "/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-xerox-dc250-heavyweight-1-progressive-jfif.jpg", + "/jpeg/no-image-types-rgb-us-web-coated-v2-ms-photogallery-exif.jpg" + ); + + for (String resource : resources) { + // Just test that we can read the metadata without exceptions + JPEGImageReader reader = createReader(); + ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource(resource)); + + try { + reader.setInput(stream); + IIOMetadata metadata = reader.getImageMetadata(0); + assertNotNull(String.format("%s: null metadata", resource), metadata); + + Node tree = metadata.getAsTree(metadata.getNativeMetadataFormatName()); + assertNotNull(tree); +// new XMLSerializer(System.err, System.getProperty("file.encoding")).serialize(tree, false); + + } + catch (IIOException e) { + AssertionError fail = new AssertionError(String.format("Reading metadata failed for %ss: %s", resource, e.getMessage())); + fail.initCause(e); + throw fail; + } + finally { + stream.close(); + } + } + } + + @Test + public void testReadMetadataEqualReference() throws IOException { + // Compares the metadata for JFIF-conformant files with metadata from com.sun...JPEGImageReader + JPEGImageReader reader = createReader(); + ImageReader referenceReader; + + try { + @SuppressWarnings("unchecked") + Class spiClass = (Class) Class.forName("com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi"); + ImageReaderSpi provider = spiClass.newInstance(); + referenceReader = provider.createReaderInstance(); + } + catch (Throwable t) { + System.err.println("WARNING: Could not create ImageReader for reference (missing dependency): " + t.getMessage()); + return; + } + + for (TestData testData : getTestData()) { + reader.setInput(testData.getInputStream()); + referenceReader.setInput(testData.getInputStream()); + + for (int i = 0; i < reader.getNumImages(true); i++) { + try { + IIOMetadata reference = referenceReader.getImageMetadata(i); + + try { + IIOMetadata metadata = reader.getImageMetadata(i); + + String[] formatNames = reference.getMetadataFormatNames(); + for (String formatName : formatNames) { + Node referenceTree = reference.getAsTree(formatName); + Node actualTree = metadata.getAsTree(formatName); + +// new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(actualTree, false); + assertTreesEquals(String.format("Metadata differs for %s image %s ", testData, i), referenceTree, actualTree); + } + } + catch (IIOException e) { + AssertionError fail = new AssertionError(String.format("Reading metadata failed for %s image %s: %s", testData, i, e.getMessage())); + fail.initCause(e); + throw fail; + } + } + catch (IIOException ignore) { + // The reference reader will fail on certain images, we'll just ignore that + System.err.println(String.format("WARNING: Reading reference metadata failed for %s image %s: %s", testData, i, ignore.getMessage())); + } + } + } + } + + private void assertTreesEquals(String message, Node expectedTree, Node actualTree) { + if (expectedTree == actualTree) { + return; + } + + if (expectedTree == null) { + assertNull(actualTree); + } + + assertEquals(String.format("%s: Node names differ", message), expectedTree.getNodeName(), actualTree.getNodeName()); + + NamedNodeMap expectedAttributes = expectedTree.getAttributes(); + NamedNodeMap actualAttributes = actualTree.getAttributes(); + assertEquals(String.format("%s: Number of attributes for <%s> differ", message, expectedTree.getNodeName()), expectedAttributes.getLength(), actualAttributes.getLength()); + for (int i = 0; i < expectedAttributes.getLength(); i++) { + Node item = expectedAttributes.item(i); + assertEquals(String.format("%s: \"%s\" attribute for <%s> differ", message, item.getNodeName(), expectedTree.getNodeName()), item.getNodeValue(), actualAttributes.getNamedItem(item.getNodeName()).getNodeValue()); + } + + // Test for equal user objects. + // - array equals or reflective equality... Most user objects does not have a decent equals method.. :-P + if (expectedTree instanceof IIOMetadataNode) { + assertTrue(String.format("%s: %s not an IIOMetadataNode", message, expectedTree.getNodeName()), actualTree instanceof IIOMetadataNode); + + Object expectedUserObject = ((IIOMetadataNode) expectedTree).getUserObject(); + + if (expectedUserObject != null) { + Object actualUserObject = ((IIOMetadataNode) actualTree).getUserObject(); + assertNotNull(String.format("%s: User object missing for <%s>", message, expectedTree.getNodeName()), actualUserObject); + assertEqualUserObjects(String.format("%s: User objects for <%s MarkerTag\"%s\"> differ", message, expectedTree.getNodeName(), ((IIOMetadataNode) expectedTree).getAttribute("MarkerTag")), expectedUserObject, actualUserObject); + } + } + + // Sort nodes to make sure that sequence of equally named tags does not matter + List expectedChildren = sortNodes(expectedTree.getChildNodes()); + List actualChildren = sortNodes(actualTree.getChildNodes()); + + assertEquals(String.format("%s: Number of child nodes for %s differ", message, expectedTree.getNodeName()), expectedChildren.size(), actualChildren.size()); + + for (int i = 0; i < expectedChildren.size(); i++) { + assertTreesEquals(message + "<" + expectedTree.getNodeName() + ">", expectedChildren.get(i), actualChildren.get(i)); + } + } + + private void assertEqualUserObjects(String message, Object expectedUserObject, Object actualUserObject) { + if (expectedUserObject.equals(actualUserObject)) { + return; + } + + if (expectedUserObject instanceof ICC_Profile) { + if (actualUserObject instanceof ICC_Profile) { + assertArrayEquals(message, ((ICC_Profile) expectedUserObject).getData(), ((ICC_Profile) actualUserObject).getData()); + return; + } + } + else if (expectedUserObject instanceof byte[]) { + if (actualUserObject instanceof byte[]) { + assertArrayEquals(message, (byte[]) expectedUserObject, (byte[]) actualUserObject); + return; + } + } + else if (expectedUserObject instanceof JPEGHuffmanTable) { + if (actualUserObject instanceof JPEGHuffmanTable) { + assertArrayEquals(message, ((JPEGHuffmanTable) expectedUserObject).getLengths(), ((JPEGHuffmanTable) actualUserObject).getLengths()); + assertArrayEquals(message, ((JPEGHuffmanTable) expectedUserObject).getValues(), ((JPEGHuffmanTable) actualUserObject).getValues()); + return; + } + } + else if (expectedUserObject instanceof JPEGQTable) { + if (actualUserObject instanceof JPEGQTable) { + assertArrayEquals(message, ((JPEGQTable) expectedUserObject).getTable(), ((JPEGQTable) actualUserObject).getTable()); + return; + } + } + + fail(expectedUserObject.getClass().getName()); + } + + private List sortNodes(final NodeList nodes) { + ArrayList sortedNodes = new ArrayList(new AbstractList() { + @Override + public IIOMetadataNode get(int index) { + return (IIOMetadataNode) nodes.item(index); + } + + @Override + public int size() { + return nodes.getLength(); + } + }); + + Collections.sort( + sortedNodes, + new Comparator() { + public int compare(IIOMetadataNode left, IIOMetadataNode right) { + int res = left.getNodeName().compareTo(right.getNodeName()); + if (res != 0) { + return res; + } + + // Compare attribute values + NamedNodeMap leftAttributes = left.getAttributes(); // TODO: We should sort left's attributes as well, for stable sorting + handle diffs in attributes + NamedNodeMap rightAttributes = right.getAttributes(); + + for (int i = 0; i < leftAttributes.getLength(); i++) { + Node leftAttribute = leftAttributes.item(i); + Node rightAttribute = rightAttributes.getNamedItem(leftAttribute.getNodeName()); + + if (rightAttribute == null) { + return 1; + } + + res = leftAttribute.getNodeValue().compareTo(rightAttribute.getNodeValue()); + if (res != 0) { + return res; + } + } + + if (left.getUserObject() instanceof byte[] && right.getUserObject() instanceof byte[]) { + byte[] leftBytes = (byte[]) left.getUserObject(); + byte[] rightBytes = (byte[]) right.getUserObject(); + + if (leftBytes.length < rightBytes.length) { + return 1; + } + + if (leftBytes.length > rightBytes.length) { + return -1; + } + + if (leftBytes.length > 0) { + for (int i = 0; i < leftBytes.length; i++) { + if (leftBytes[i] < rightBytes[i]) { + return -1; + } + if (leftBytes[i] > rightBytes[i]) { + return 1; + } + } + } + } + + return 0; + } + } + ); + + return sortedNodes; + } } diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStreamTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStreamTest.java index 4027a56d..b42fa9d2 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStreamTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStreamTest.java @@ -77,7 +77,7 @@ public class JPEGSegmentImageInputStreamTest { public void testStreamRealData() throws IOException { ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-internal-kodak-srgb-jfif.jpg"))); assertEquals(JPEG.SOI, stream.readUnsignedShort()); - assertEquals(JPEG.APP0, stream.readUnsignedShort()); + assertEquals(JPEG.DQT, stream.readUnsignedShort()); } @Test @@ -88,7 +88,7 @@ public class JPEGSegmentImageInputStreamTest { // NOTE: read(byte[], int, int) must always read len bytes (or until EOF), due to known bug in Sun code assertEquals(20, stream.read(bytes, 0, 20)); - assertArrayEquals(new byte[] {(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xE0, 0x0, 0x10, 'J', 'F', 'I', 'F', 0x0, 0x1, 0x1, 0x1, 0x1, (byte) 0xCC, 0x1, (byte) 0xCC, 0, 0}, bytes); + assertArrayEquals(new byte[] {(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xDB, 0x0, 0x43, 0x0, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1}, bytes); } @Test @@ -102,7 +102,7 @@ public class JPEGSegmentImageInputStreamTest { assertThat(length, new LessOrEqual(10203l)); // In no case should length increase - assertEquals(9625l, length); // May change, if more chunks are passed to reader... + assertEquals(9607L, length); // May change, if more chunks are passed to reader... } @Test @@ -110,18 +110,15 @@ public class JPEGSegmentImageInputStreamTest { ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/no-image-types-rgb-us-web-coated-v2-ms-photogallery-exif.jpg"))); List appSegments = JPEGSegmentUtil.readSegments(stream, JPEGSegmentUtil.APP_SEGMENTS); - assertEquals(3, appSegments.size()); + assertEquals(2, appSegments.size()); - assertEquals(JPEG.APP0, appSegments.get(0).marker()); - assertEquals("JFIF", appSegments.get(0).identifier()); + assertEquals(JPEG.APP1, appSegments.get(0).marker()); + assertEquals("Exif", appSegments.get(0).identifier()); - assertEquals(JPEG.APP1, appSegments.get(1).marker()); - assertEquals("Exif", appSegments.get(1).identifier()); + assertEquals(JPEG.APP14, appSegments.get(1).marker()); + assertEquals("Adobe", appSegments.get(1).identifier()); - assertEquals(JPEG.APP14, appSegments.get(2).marker()); - assertEquals("Adobe", appSegments.get(2).identifier()); - - // And thus, no XMP, no ICC_PROFILE or other segments + // And thus, no JFIF, no XMP, no ICC_PROFILE or other segments } @Test @@ -133,7 +130,7 @@ public class JPEGSegmentImageInputStreamTest { length++; } - assertEquals(9299l, length); // Sanity check: same as file size + assertEquals(9281L, length); // Sanity check: same as file size } @Test @@ -141,13 +138,10 @@ public class JPEGSegmentImageInputStreamTest { ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-padded-segments.jpg"))); List appSegments = JPEGSegmentUtil.readSegments(stream, JPEGSegmentUtil.APP_SEGMENTS); - assertEquals(2, appSegments.size()); + assertEquals(1, appSegments.size()); - assertEquals(JPEG.APP0, appSegments.get(0).marker()); - assertEquals("JFIF", appSegments.get(0).identifier()); - - assertEquals(JPEG.APP1, appSegments.get(1).marker()); - assertEquals("Exif", appSegments.get(1).identifier()); + assertEquals(JPEG.APP1, appSegments.get(0).marker()); + assertEquals("Exif", appSegments.get(0).identifier()); stream.seek(0l); @@ -156,6 +150,6 @@ public class JPEGSegmentImageInputStreamTest { length++; } - assertEquals(1079L, length); // Sanity check: same as file size, except padding and the filtered ICC_PROFILE segment + assertEquals(1061L, length); // Sanity check: same as file size, except padding and the filtered ICC_PROFILE segment } } From ca48837e11edfef8c6c9b8d940df73adacfeffde Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 23 Oct 2013 14:56:01 +0200 Subject: [PATCH 26/98] TMI-JPEG-4: Moved metadata cleaning to separate class. Better class name welcome... ;-) --- .../jpeg/JPEGImage10MetadataCleaner.java | 270 ++++++++++++++++ .../imageio/plugins/jpeg/JPEGImageReader.java | 306 +++--------------- 2 files changed, 309 insertions(+), 267 deletions(-) create mode 100644 imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java new file mode 100644 index 00000000..791ffd13 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java @@ -0,0 +1,270 @@ +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; +import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment; +import com.twelvemonkeys.xml.XMLSerializer; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.imageio.metadata.IIOInvalidTreeException; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import java.awt.color.ICC_Profile; +import java.io.DataInputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.List; + +/** + * JPEGImage10MetadataCleaner + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JPEGImage10MetadataCleaner.java,v 1.0 22.10.13 14:41 haraldk Exp$ + */ +final class JPEGImage10MetadataCleaner { + + /** + * Native metadata format name + */ + static final String JAVAX_IMAGEIO_JPEG_IMAGE_1_0 = "javax_imageio_jpeg_image_1.0"; + + private final JPEGImageReader reader; + + JPEGImage10MetadataCleaner(JPEGImageReader reader) { + this.reader = reader; + } + + IIOMetadata cleanMetadata(final int imageIndex, final IIOMetadata imageMetadata, final List appSegments) throws IOException { + // We filter out pretty much everything from the stream.. + // Meaning we have to read get *all APP segments* and re-insert into metadata. + + // NOTE: There's a bug in the merging code in JPEGMetadata mergeUnknownNode that makes sure all "unknown" nodes are added twice in certain conditions.... ARGHBL... + // DONE: 1: Work around + // TODO: 2: REPORT BUG! + // TODO: Report dht inconsistency bug (reads any amount of tables but only allows setting 4 tables) + + // TODO: Allow EXIF (as app1EXIF) in the JPEGvariety (sic) node. Need new format, might as well create a completely new format... + // As EXIF is (a subset of) TIFF, (and the EXIF data is a valid TIFF stream) probably use something like: + // http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata + /* + from: http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html + + In future versions of the JPEG metadata format, other varieties of JPEG metadata may be supported (e.g. Exif) + by defining other types of nodes which may appear as a child of the JPEGvariety node. + + (Note that an application wishing to interpret Exif metadata given a metadata tree structure in the + javax_imageio_jpeg_image_1.0 format must check for an unknown marker segment with a tag indicating an + APP1 marker and containing data identifying it as an Exif marker segment. Then it may use application-specific + code to interpret the data in the marker segment. If such an application were to encounter a metadata tree + formatted according to a future version of the JPEG metadata format, the Exif marker segment might not be + unknown in that format - it might be structured as a child node of the JPEGvariety node. + + Thus, it is important for an application to specify which version to use by passing the string identifying + the version to the method/constructor used to obtain an IIOMetadata object.) + */ + + IIOMetadataNode tree = (IIOMetadataNode) imageMetadata.getAsTree(JAVAX_IMAGEIO_JPEG_IMAGE_1_0); + IIOMetadataNode jpegVariety = (IIOMetadataNode) tree.getElementsByTagName("JPEGvariety").item(0); + IIOMetadataNode markerSequence = (IIOMetadataNode) tree.getElementsByTagName("markerSequence").item(0); + + JFIFSegment jfifSegment = reader.getJFIF(); + JFXXSegment jfxxSegment = reader.getJFXX(); + AdobeDCTSegment adobeDCT = reader.getAdobeDCT(); + ICC_Profile embeddedICCProfile = reader.getEmbeddedICCProfile(true); + SOFSegment sof = reader.getSOF(); + + boolean hasRealJFIF = false; + boolean hasRealJFXX = false; + boolean hasRealICC = false; + + if (jfifSegment != null) { + // Normal case, conformant JFIF with 1 or 3 components + // TODO: Test if we have CMY or other non-JFIF color space? + if (sof.componentsInFrame() == 1 || sof.componentsInFrame() == 3) { + IIOMetadataNode jfif = new IIOMetadataNode("app0JFIF"); + jfif.setAttribute("majorVersion", String.valueOf(jfifSegment.majorVersion)); + jfif.setAttribute("minorVersion", String.valueOf(jfifSegment.minorVersion)); + jfif.setAttribute("resUnits", String.valueOf(jfifSegment.units)); + jfif.setAttribute("Xdensity", String.valueOf(jfifSegment.xDensity)); + jfif.setAttribute("Ydensity", String.valueOf(jfifSegment.yDensity)); + jfif.setAttribute("thumbWidth", String.valueOf(jfifSegment.xThumbnail)); + jfif.setAttribute("thumbHeight", String.valueOf(jfifSegment.yThumbnail)); + + jpegVariety.appendChild(jfif); + hasRealJFIF = true; + + // Add app2ICC and JFXX as proper nodes + if (embeddedICCProfile != null) { + IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC"); + app2ICC.setUserObject(embeddedICCProfile); + jfif.appendChild(app2ICC); + hasRealICC = true; + } + + if (jfxxSegment != null) { + IIOMetadataNode JFXX = new IIOMetadataNode("JFXX"); + jfif.appendChild(JFXX); + IIOMetadataNode app0JFXX = new IIOMetadataNode("app0JFXX"); + app0JFXX.setAttribute("extensionCode", String.valueOf(jfxxSegment.extensionCode)); + + JFXXThumbnailReader thumbnailReader = new JFXXThumbnailReader(null, reader.getThumbnailReader(), imageIndex, -1, jfxxSegment); + IIOMetadataNode jfifThumb; + + switch (jfxxSegment.extensionCode) { + case JFXXSegment.JPEG: + jfifThumb = new IIOMetadataNode("JFIFthumbJPEG"); + // Contains it's own "markerSequence" with full DHT, DQT, SOF etc... + IIOMetadata thumbMeta = thumbnailReader.readMetadata(); + Node thumbTree = thumbMeta.getAsTree(JAVAX_IMAGEIO_JPEG_IMAGE_1_0); + jfifThumb.appendChild(thumbTree.getLastChild()); + app0JFXX.appendChild(jfifThumb); + break; + + case JFXXSegment.INDEXED: + jfifThumb = new IIOMetadataNode("JFIFthumbPalette"); + jfifThumb.setAttribute("thumbWidth", String.valueOf(thumbnailReader.getWidth())); + jfifThumb.setAttribute("thumbHeight", String.valueOf(thumbnailReader.getHeight())); + app0JFXX.appendChild(jfifThumb); + break; + + case JFXXSegment.RGB: + jfifThumb = new IIOMetadataNode("JFIFthumbRGB"); + jfifThumb.setAttribute("thumbWidth", String.valueOf(thumbnailReader.getWidth())); + jfifThumb.setAttribute("thumbHeight", String.valueOf(thumbnailReader.getHeight())); + app0JFXX.appendChild(jfifThumb); + break; + + default: + reader.processWarningOccurred(String.format("Unknown JFXX extension code: %d", jfxxSegment.extensionCode)); + } + + JFXX.appendChild(app0JFXX); + hasRealJFXX = true; + } + } + else { + // Typically CMYK JPEG with JFIF segment (Adobe or similar). + reader.processWarningOccurred(String.format( + "Incompatible JFIF marker segment in stream. " + + "SOF%d has %d color components, JFIF allows only 1 or 3 components. Ignoring JFIF marker.", + sof.marker & 0xf, sof.componentsInFrame() + )); + } + } + + // Special case: Broken AdobeDCT segment, inconsistent with SOF, use values from SOF + if (adobeDCT != null && adobeDCT.getTransform() == AdobeDCTSegment.YCCK && sof.componentsInFrame() < 4) { + reader.processWarningOccurred(String.format( + "Invalid Adobe App14 marker. Indicates YCCK/CMYK data, but SOF%d has %d color components. " + + "Ignoring Adobe App14 marker.", + sof.marker & 0xf, sof.componentsInFrame() + )); + + // Remove bad AdobeDCT + NodeList app14Adobe = tree.getElementsByTagName("app14Adobe"); + for (int i = app14Adobe.getLength() - 1; i >= 0; i--) { + Node item = app14Adobe.item(i); + item.getParentNode().removeChild(item); + } + + // We don't add this as unknown marker, as we are certain it's bogus by now + } + + Node next = null; + for (JPEGSegment segment : appSegments) { + // Except real app0JFIF, app0JFXX, app2ICC and app14Adobe, add all the app segments that we filtered away as "unknown" markers + if (segment.marker() == JPEG.APP0 && "JFIF".equals(segment.identifier()) && hasRealJFIF) { + continue; + } + else if (segment.marker() == JPEG.APP0 && "JFXX".equals(segment.identifier()) && hasRealJFXX) { + continue; + } + else if (segment.marker() == JPEG.APP1 && "Exif".equals(segment.identifier()) /* always inserted */) { + continue; + } + else if (segment.marker() == JPEG.APP2 && "ICC_PROFILE".equals(segment.identifier()) && hasRealICC) { + continue; + } + else if (segment.marker() == JPEG.APP14 && "Adobe".equals(segment.identifier()) /* always inserted */) { + continue; + } + + IIOMetadataNode unknown = new IIOMetadataNode("unknown"); + unknown.setAttribute("MarkerTag", Integer.toString(segment.marker() & 0xff)); + + DataInputStream stream = new DataInputStream(segment.data()); + + try { + String identifier = segment.identifier(); + int off = identifier != null ? identifier.length() + 1 : 0; + + byte[] data = new byte[off + segment.length()]; + + if (identifier != null) { + System.arraycopy(identifier.getBytes(Charset.forName("ASCII")), 0, data, 0, identifier.length()); + } + + stream.readFully(data, off, segment.length()); + + unknown.setUserObject(data); + } + finally { + stream.close(); + } + + if (next == null) { + // To be semi-compatible with the functionality in mergeTree, + // let's insert after the last unknown tag, or before any other tag if no unknown tag exists + NodeList unknowns = markerSequence.getElementsByTagName("unknown"); + + if (unknowns.getLength() > 0) { + next = unknowns.item(unknowns.getLength() - 1).getNextSibling(); + } + else { + next = markerSequence.getFirstChild(); + } + } + + markerSequence.insertBefore(unknown, next); + } + + // Inconsistency issue in the com.sun classes, it can read metadata with dht containing + // more than 4 children, but will not allow setting such a tree... + // We'll split AC/DC tables into separate dht nodes. + NodeList dhts = markerSequence.getElementsByTagName("dht"); + for (int j = 0; j < dhts.getLength(); j++) { + Node dht = dhts.item(j); + NodeList dhtables = dht.getChildNodes(); + + if (dhtables.getLength() > 4) { + IIOMetadataNode acTables = new IIOMetadataNode("dht"); + dht.getParentNode().insertBefore(acTables, dht.getNextSibling()); + + // Split into 2 dht nodes, one for AC and one for DC + for (int i = 0; i < dhtables.getLength(); i++) { + Element dhtable = (Element) dhtables.item(i); + String tableClass = dhtable.getAttribute("class"); + if ("1".equals(tableClass)) { + dht.removeChild(dhtable); + acTables.appendChild(dhtable); + } + } + } + } + + try { + imageMetadata.setFromTree(JAVAX_IMAGEIO_JPEG_IMAGE_1_0, tree); + } + catch (IIOInvalidTreeException e) { + if (JPEGImageReader.DEBUG) { + new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(tree, false); + } + + throw e; + } + + return imageMetadata; + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 210975cb..2940e5ff 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -42,16 +42,11 @@ import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; import com.twelvemonkeys.imageio.util.ProgressListenerBase; import com.twelvemonkeys.lang.Validate; import com.twelvemonkeys.xml.XMLSerializer; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; import javax.imageio.*; import javax.imageio.event.IIOReadUpdateListener; import javax.imageio.event.IIOReadWarningListener; -import javax.imageio.metadata.IIOInvalidTreeException; import javax.imageio.metadata.IIOMetadata; -import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.MemoryCacheImageInputStream; @@ -61,7 +56,6 @@ import java.awt.color.ICC_ColorSpace; import java.awt.color.ICC_Profile; import java.awt.image.*; import java.io.*; -import java.nio.charset.Charset; import java.util.*; import java.util.List; @@ -84,10 +78,17 @@ import java.util.List; *

* Thumbnail support: *
    - *
  • Support for JFIF thumbnails (even if stream contains "inconsistent metadata")
  • + *
  • Support for JFIF thumbnails (even if stream contains inconsistent metadata)
  • *
  • Support for JFXX thumbnails (JPEG, Indexed and RGB)
  • *
  • Support for EXIF thumbnails (JPEG, RGB and YCbCr)
  • *
+ * Metadata support: + *
    + *
  • Support for JPEG metadata in both standard and native formats (even if stream contains inconsistent metadata)
  • + *
  • Support for {@code javax_imageio_jpeg_image_1.0} format (currently as native format, may change in the future)
  • + *
  • Support for illegal combinations of JFIF, Exif and Adobe markers, using "unknown" segments in the + * "MarkerSequence" tag for the unsupported segments (for {@code javax_imageio_jpeg_image_1.0} format)
  • + *
* * @author Harald Kuhr * @author LUT-based YCbCR conversion by Werner Randelshofer @@ -95,12 +96,10 @@ import java.util.List; * @version $Id: JPEGImageReader.java,v 1.0 24.01.11 16.37 haraldk Exp$ */ public class JPEGImageReader extends ImageReaderBase { - // TODO: Fix the (stream) metadata inconsistency issues. - // - Sun JPEGMetadata class does not (and can not be made to) support CMYK data.. We need to create all new metadata classes.. :-/ - // TODO: Allow automatic rotation based on EXIF rotation field? + // TODO: Create a simplified native metadata format that is closer to the actual JPEG stream AND supports EXIF in a sensible way - private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug")); + final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug")); /** Internal constant for referring all APP segments */ private static final int ALL_APP_MARKERS = -1; @@ -111,20 +110,6 @@ public class JPEGImageReader extends ImageReaderBase { private static Map> createSegmentIds() { Map> map = new LinkedHashMap>(); - /* - // JFIF/JFXX APP0 markers - map.put(JPEG.APP0, JPEGSegmentUtil.ALL_IDS); - - // Exif metadata - map.put(JPEG.APP1, Collections.singletonList("Exif")); - - // ICC Color Profile - map.put(JPEG.APP2, Collections.singletonList("ICC_PROFILE")); - - // Adobe APP14 marker - map.put(JPEG.APP14, Collections.singletonList("Adobe")); - //*/ - // Need all APP markers to be able to re-generate proper metadata later for (int appMarker = JPEG.APP0; appMarker <= JPEG.APP15; appMarker++) { map.put(appMarker, JPEGSegmentUtil.ALL_IDS); @@ -150,16 +135,19 @@ public class JPEGImageReader extends ImageReaderBase { /** Our JPEG reading delegate */ private final ImageReader delegate; - private ImageReader thumbnailReader; /** Listens to progress updates in the delegate, and delegates back to this instance */ private final ProgressDelegator progressDelegator; - /** Cached JPEG app segments */ - private List segments; - + /** Extra delegate for reading JPEG encoded thumbnails */ + private ImageReader thumbnailReader; private List thumbnails; + private JPEGImage10MetadataCleaner metadataCleaner; + + /** Cached list of JPEG segments we filter from the underlying stream */ + private List segments; + JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) { super(provider); this.delegate = Validate.notNull(delegate); @@ -183,6 +171,8 @@ public class JPEGImageReader extends ImageReaderBase { thumbnailReader.reset(); } + metadataCleaner = null; + installListeners(); } @@ -712,7 +702,7 @@ public class JPEGImageReader extends ImageReaderBase { return appSegments; } - private SOFSegment getSOF() throws IOException { + SOFSegment getSOF() throws IOException { for (JPEGSegment segment : segments) { if (JPEG.SOF0 >= segment.marker() && segment.marker() <= JPEG.SOF3 || JPEG.SOF5 >= segment.marker() && segment.marker() <= JPEG.SOF7 || @@ -748,7 +738,7 @@ public class JPEGImageReader extends ImageReaderBase { return null; } - private AdobeDCTSegment getAdobeDCT() throws IOException { + AdobeDCTSegment getAdobeDCT() throws IOException { // TODO: Investigate http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6355567: 33/35 byte Adobe APP14 markers List adobe = getAppSegments(JPEG.APP14, "Adobe"); @@ -767,7 +757,7 @@ public class JPEGImageReader extends ImageReaderBase { return null; } - private JFIFSegment getJFIF() throws IOException{ + JFIFSegment getJFIF() throws IOException{ List jfif = getAppSegments(JPEG.APP0, "JFIF"); if (!jfif.isEmpty()) { @@ -778,7 +768,7 @@ public class JPEGImageReader extends ImageReaderBase { return null; } - private JFXXSegment getJFXX() throws IOException { + JFXXSegment getJFXX() throws IOException { List jfxx = getAppSegments(JPEG.APP0, "JFXX"); if (!jfxx.isEmpty()) { @@ -821,7 +811,7 @@ public class JPEGImageReader extends ImageReaderBase { return data; } - private ICC_Profile getEmbeddedICCProfile(final boolean allowBadIndexes) throws IOException { + ICC_Profile getEmbeddedICCProfile(final boolean allowBadIndexes) throws IOException { // ICC v 1.42 (2006) annex B: // APP2 marker (0xFFE2) + 2 byte length + ASCII 'ICC_PROFILE' + 0 (termination) // + 1 byte chunk number + 1 byte chunk count (allows ICC profiles chunked in multiple APP2 segments) @@ -994,7 +984,7 @@ public class JPEGImageReader extends ImageReaderBase { } } - private ImageReader getThumbnailReader() throws IOException { + ImageReader getThumbnailReader() throws IOException { if (thumbnailReader == null) { thumbnailReader = delegate.getOriginatingProvider().createReaderInstance(); } @@ -1039,243 +1029,19 @@ public class JPEGImageReader extends ImageReaderBase { @Override public IIOMetadata getImageMetadata(int imageIndex) throws IOException { - // TODO: Extract metadata handling in separate class, for less mess and easier testing - // We filter out pretty much everything from the stream.. - // Meaning we have to read *all APP segments* and re-insert into metadata. + IIOMetadata imageMetadata = delegate.getImageMetadata(imageIndex); - // TODO: There's a bug in the merging code in JPEGMetadata mergeUnknownNode that makes sure all "unknown" nodes are added twice in certain conditions.... ARGHBL... - // TODO: 1: Work around - // TODO: 2: REPORT BUG! - - List appSegments = getAppSegments(ALL_APP_MARKERS, null); -// System.out.println("appSegments: " + appSegments); - - IIOMetadata metadata = delegate.getImageMetadata(imageIndex); - - if (metadata != null) { - String format = metadata.getNativeMetadataFormatName(); - IIOMetadataNode tree = (IIOMetadataNode) metadata.getAsTree(format); - IIOMetadataNode jpegVariety = (IIOMetadataNode) tree.getElementsByTagName("JPEGvariety").item(0); - IIOMetadataNode markerSequence = (IIOMetadataNode) tree.getElementsByTagName("markerSequence").item(0); - - JFIFSegment jfifSegment = getJFIF(); - JFXXSegment jfxxSegment = getJFXX(); - AdobeDCTSegment adobeDCT = getAdobeDCT(); - ICC_Profile embeddedICCProfile = getEmbeddedICCProfile(true); - SOFSegment sof = getSOF(); - - boolean hasRealJFIF = false; - boolean hasRealJFXX = false; - boolean hasRealICC = false; - - if (jfifSegment != null) { - // Normal case, conformant JFIF with 1 or 3 components - // TODO: Test if we have CMY or other bad color space? - // TODO: Remove JFIF if app14Adobe transform is YCCK (and isn't incorrect...) - if (sof.componentsInFrame() == 1 || sof.componentsInFrame() == 3) { - IIOMetadataNode jfif = new IIOMetadataNode("app0JFIF"); - jfif.setAttribute("majorVersion", String.valueOf(jfifSegment.majorVersion)); - jfif.setAttribute("minorVersion", String.valueOf(jfifSegment.minorVersion)); - jfif.setAttribute("resUnits", String.valueOf(jfifSegment.units)); - jfif.setAttribute("Xdensity", String.valueOf(jfifSegment.xDensity)); - jfif.setAttribute("Ydensity", String.valueOf(jfifSegment.yDensity)); - jfif.setAttribute("thumbWidth", String.valueOf(jfifSegment.xThumbnail)); - jfif.setAttribute("thumbHeight", String.valueOf(jfifSegment.yThumbnail)); - - jpegVariety.appendChild(jfif); - hasRealJFIF = true; - - // Add app2ICC and JFXX as proper nodes - if (embeddedICCProfile != null) { - IIOMetadataNode app2ICC = new IIOMetadataNode("app2ICC"); - app2ICC.setUserObject(embeddedICCProfile); - jfif.appendChild(app2ICC); - hasRealICC = true; - } - - if (jfxxSegment != null) { - IIOMetadataNode JFXX = new IIOMetadataNode("JFXX"); - jfif.appendChild(JFXX); - IIOMetadataNode app0JFXX = new IIOMetadataNode("app0JFXX"); - app0JFXX.setAttribute("extensionCode", String.valueOf(jfxxSegment.extensionCode)); - - JFXXThumbnailReader reader = new JFXXThumbnailReader(null, getThumbnailReader(), imageIndex, -1, jfxxSegment); - - IIOMetadataNode jfifThumb; - switch (jfxxSegment.extensionCode) { - case JFXXSegment.JPEG: - jfifThumb = new IIOMetadataNode("JFIFthumbJPEG"); - // Contains it's own "markerSequence" with full DHT, DQT, SOF etc... - IIOMetadata thumbMeta = reader.readMetadata(); - Node thumbTree = thumbMeta.getAsTree(format); - jfifThumb.appendChild(thumbTree.getLastChild()); - app0JFXX.appendChild(jfifThumb); - break; - - case JFXXSegment.INDEXED: - jfifThumb = new IIOMetadataNode("JFIFthumbPalette"); - jfifThumb.setAttribute("thumbWidth", String.valueOf(reader.getWidth())); - jfifThumb.setAttribute("thumbHeight", String.valueOf(reader.getHeight())); - app0JFXX.appendChild(jfifThumb); - break; - - case JFXXSegment.RGB: - jfifThumb = new IIOMetadataNode("JFIFthumbRGB"); - jfifThumb.setAttribute("thumbWidth", String.valueOf(reader.getWidth())); - jfifThumb.setAttribute("thumbHeight", String.valueOf(reader.getHeight())); - app0JFXX.appendChild(jfifThumb); - break; - - default: - processWarningOccurred(String.format("Unknown JFXX extension code: %d", jfxxSegment.extensionCode)); - } - - JFXX.appendChild(app0JFXX); - hasRealJFXX = true; - } - } - else { - // Typically CMYK with JFIF segment (Adobe or similar). - processWarningOccurred(String.format( - "Incompatible JFIF marker segment in stream. " + - "SOF%d has %d color components, JFIF allows only 1 or 3 components. Ignoring JFIF marker.", - sof.marker & 0xf, sof.componentsInFrame() - )); - } + if (imageMetadata != null && Arrays.asList(imageMetadata.getMetadataFormatNames()).contains(JPEGImage10MetadataCleaner.JAVAX_IMAGEIO_JPEG_IMAGE_1_0)) { + if (metadataCleaner == null) { + metadataCleaner = new JPEGImage10MetadataCleaner(this); } - // Special case: Broken AdobeDCT segment, inconsistent with SOF, use values from SOF - if (adobeDCT != null && adobeDCT.getTransform() == AdobeDCTSegment.YCCK && sof.componentsInFrame() < 4) { - processWarningOccurred(String.format( - "Invalid Adobe App14 marker. Indicates YCCK/CMYK data, but SOF%d has %d color components. " + - "Ignoring Adobe App14 marker.", - sof.marker & 0xf, sof.componentsInFrame() - )); + List appSegments = getAppSegments(JPEGImageReader.ALL_APP_MARKERS, null); - // Remove bad AdobeDCT - NodeList app14Adobe = tree.getElementsByTagName("app14Adobe"); - for (int i = app14Adobe.getLength() - 1; i >= 0; i--) { - Node item = app14Adobe.item(i); - item.getParentNode().removeChild(item); - } - - - // TODO: Add app14 as "unknown" marker? -// IIOMetadataNode app14Adobe = new IIOMetadataNode("app14Adobe"); -// app14Adobe.setAttribute("version", String.valueOf(adobeDCT.getVersion())); -// app14Adobe.setAttribute("flags0", String.valueOf(adobeDCT.getFlags0())); -// app14Adobe.setAttribute("flags1", String.valueOf(adobeDCT.getFlags1())); -// app14Adobe.setAttribute("transform", String.valueOf(sof.componentsInFrame() == 3 ? )); -// markerSequence.appendChild(app14Adobe); - } - - Node next = null; - for (JPEGSegment segment : appSegments) { - // TODO: Except real app0JFIF, app0JFXX, app2ICC and app14Adobe, add all the app segments that we filtered away as "unknown" markers - if (segment.marker() == JPEG.APP0 && "JFIF".equals(segment.identifier()) && hasRealJFIF) { - continue; - } - else if (segment.marker() == JPEG.APP0 && "JFXX".equals(segment.identifier()) && hasRealJFXX) { - continue; - } - else if (segment.marker() == JPEG.APP1 && "Exif".equals(segment.identifier()) /* always inserted */) { - continue; - } - else if (segment.marker() == JPEG.APP2 && "ICC_PROFILE".equals(segment.identifier()) && hasRealICC) { - continue; - } - else if (segment.marker() == JPEG.APP14 && "Adobe".equals(segment.identifier()) /* always inserted */) { - continue; - } - - IIOMetadataNode unknown = new IIOMetadataNode("unknown"); - unknown.setAttribute("MarkerTag", Integer.toString(segment.marker() & 0xff)); - - DataInputStream stream = new DataInputStream(segment.data()); - - try { - String identifier = segment.identifier(); - int off = identifier != null ? identifier.length() + 1 : 0; - - byte[] data = new byte[off + segment.length()]; - - if (identifier != null) { - System.arraycopy(identifier.getBytes(Charset.forName("ASCII")), 0, data, 0, identifier.length()); - } - - stream.readFully(data, off, segment.length()); - - unknown.setUserObject(data); - } - finally { - stream.close(); - } - - if (next == null) { - next = markerSequence.getFirstChild(); - } - - markerSequence.insertBefore(unknown, next); - } - - // Inconsistency issue in the com.sun classes, it can read metadata with dht containing - // more than 4 children, but will not allow setting such a tree... - // We'll split AC/DC tables into separate dht nodes. - NodeList dhts = markerSequence.getElementsByTagName("dht"); - for (int j = 0; j < dhts.getLength(); j++) { - Node dht = dhts.item(j); - NodeList dhtables = dht.getChildNodes(); - - if (dhtables.getLength() > 4) { - IIOMetadataNode acTables = new IIOMetadataNode("dht"); - dht.getParentNode().insertBefore(acTables, dht.getNextSibling()); - - - // Split into 2 dht nodes, one for AC and one for DC - for (int i = 0; i < dhtables.getLength(); i++) { - Element dhtable = (Element) dhtables.item(i); - String tableClass = dhtable.getAttribute("class"); - if ("1".equals(tableClass)) { - dht.removeChild(dhtable); - acTables.appendChild(dhtable); - } - } - } - } - - // TODO: Allow EXIF (as app1EXIF) in the JPEGvariety (sic) node. - // As EXIF is (a subset of) TIFF, (and the EXIF data is a valid TIFF stream) probably use something like: - // http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata - /* - from: http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html - - In future versions of the JPEG metadata format, other varieties of JPEG metadata may be supported (e.g. Exif) - by defining other types of nodes which may appear as a child of the JPEGvariety node. - - (Note that an application wishing to interpret Exif metadata given a metadata tree structure in the - javax_imageio_jpeg_image_1.0 format must check for an unknown marker segment with a tag indicating an - APP1 marker and containing data identifying it as an Exif marker segment. Then it may use application-specific - code to interpret the data in the marker segment. If such an application were to encounter a metadata tree - formatted according to a future version of the JPEG metadata format, the Exif marker segment might not be - unknown in that format - it might be structured as a child node of the JPEGvariety node. - - Thus, it is important for an application to specify which version to use by passing the string identifying - the version to the method/constructor used to obtain an IIOMetadata object.) - */ - -// new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(tree, false); - - try { - metadata.setFromTree(format, tree); - } - catch (IIOInvalidTreeException e) { - new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(tree, false); - throw e; - } -// metadata.mergeTree(format, tree); // TODO: Merging does not work, as the "unknown" tags duplicate insert bug ruins everything. Try set instead... + return metadataCleaner.cleanMetadata(imageIndex, imageMetadata, appSegments); } - return metadata; + return imageMetadata; } @Override @@ -1283,6 +1049,11 @@ public class JPEGImageReader extends ImageReaderBase { return delegate.getStreamMetadata(); } + @Override + protected void processWarningOccurred(String warning) { + super.processWarningOccurred(warning); + } + private static void invertCMYK(final Raster raster) { byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); @@ -1612,7 +1383,8 @@ public class JPEGImageReader extends ImageReaderBase { showIt(thumbnail, String.format("Thumbnail: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight())); } - reader.getImageMetadata(0); + IIOMetadata imageMetadata = reader.getImageMetadata(0); + new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(imageMetadata.getAsTree(imageMetadata.getNativeMetadataFormatName()), false); } catch (IIOException e) { System.err.println("Could not read thumbnails: " + arg + ": " + e.getMessage()); From d7958fc8a7b46e602ae6ec4847a5c2536c4b8d31 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 23 Oct 2013 16:37:11 +0200 Subject: [PATCH 27/98] TMI-JPEG-4: Clean up --- .../imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java | 7 ++++--- .../imageio/plugins/jpeg/JPEGImageReader.java | 8 +++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java index 791ffd13..2082eaf0 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java @@ -32,13 +32,14 @@ final class JPEGImage10MetadataCleaner { private final JPEGImageReader reader; - JPEGImage10MetadataCleaner(JPEGImageReader reader) { + JPEGImage10MetadataCleaner(final JPEGImageReader reader) { this.reader = reader; } - IIOMetadata cleanMetadata(final int imageIndex, final IIOMetadata imageMetadata, final List appSegments) throws IOException { + IIOMetadata cleanMetadata(final IIOMetadata imageMetadata) throws IOException { // We filter out pretty much everything from the stream.. // Meaning we have to read get *all APP segments* and re-insert into metadata. + List appSegments = reader.getAppSegments(JPEGImageReader.ALL_APP_MARKERS, null); // NOTE: There's a bug in the merging code in JPEGMetadata mergeUnknownNode that makes sure all "unknown" nodes are added twice in certain conditions.... ARGHBL... // DONE: 1: Work around @@ -109,7 +110,7 @@ final class JPEGImage10MetadataCleaner { IIOMetadataNode app0JFXX = new IIOMetadataNode("app0JFXX"); app0JFXX.setAttribute("extensionCode", String.valueOf(jfxxSegment.extensionCode)); - JFXXThumbnailReader thumbnailReader = new JFXXThumbnailReader(null, reader.getThumbnailReader(), imageIndex, -1, jfxxSegment); + JFXXThumbnailReader thumbnailReader = new JFXXThumbnailReader(null, reader.getThumbnailReader(), 0, 0, jfxxSegment); IIOMetadataNode jfifThumb; switch (jfxxSegment.extensionCode) { diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 2940e5ff..0b798372 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -102,7 +102,7 @@ public class JPEGImageReader extends ImageReaderBase { final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug")); /** Internal constant for referring all APP segments */ - private static final int ALL_APP_MARKERS = -1; + static final int ALL_APP_MARKERS = -1; /** Segment identifiers for the JPEG segments we care about reading. */ private static final Map> SEGMENT_IDENTIFIERS = createSegmentIds(); @@ -683,7 +683,7 @@ public class JPEGImageReader extends ImageReaderBase { } } - private List getAppSegments(final int marker, final String identifier) throws IOException { + List getAppSegments(final int marker, final String identifier) throws IOException { initHeader(); List appSegments = Collections.emptyList(); @@ -1036,9 +1036,7 @@ public class JPEGImageReader extends ImageReaderBase { metadataCleaner = new JPEGImage10MetadataCleaner(this); } - List appSegments = getAppSegments(JPEGImageReader.ALL_APP_MARKERS, null); - - return metadataCleaner.cleanMetadata(imageIndex, imageMetadata, appSegments); + return metadataCleaner.cleanMetadata(imageMetadata); } return imageMetadata; From 86921ad389270f99b03677ff8f2e4dd8b1eebf07 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 24 Oct 2013 21:19:52 +0200 Subject: [PATCH 28/98] ImageReader subsampling test --- .../util/ImageReaderAbstractTestCase.java | 89 +++++++++++++------ .../plugins/icns/ICNSImageReaderTest.java | 14 ++- .../imageio/plugins/iff/IFFImageReader.java | 32 +++---- 3 files changed, 89 insertions(+), 46 deletions(-) diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTestCase.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTestCase.java index cf15f1f0..9e0be9a2 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTestCase.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTestCase.java @@ -28,6 +28,7 @@ package com.twelvemonkeys.imageio.util; +import com.twelvemonkeys.image.ImageUtil; import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi; import org.junit.Ignore; import org.junit.Test; @@ -45,9 +46,7 @@ import java.awt.*; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; -import java.io.File; import java.io.IOException; -import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; @@ -75,29 +74,6 @@ public abstract class ImageReaderAbstractTestCase { protected abstract List getTestData(); - /** - * Convenience method to get a list of test files from the classpath. - * Currently only works for resources on the filesystem (not in jars or - * archives). - * - * @param pResourceInFolder a resource in the correct classpath folder. - * @return a list of files - */ - protected final List getInputsFromClasspath(final String pResourceInFolder) { - URL resource = getClass().getClassLoader().getResource(pResourceInFolder); - assertNotNull(resource); - File dir; - try { - dir = new File(resource.toURI()).getParentFile(); - } - catch (URISyntaxException e) { - throw new RuntimeException(e); - } - List files = Arrays.asList(dir.listFiles()); - assertFalse(files.isEmpty()); - return files; - } - protected abstract ImageReaderSpi createProvider(); protected abstract Class getReaderClass(); @@ -476,7 +452,7 @@ public abstract class ImageReaderAbstractTestCase { } @Test - public void testReadWithSubsampleParam() { + public void testReadWithSubsampleParamDimensions() { ImageReader reader = createReader(); TestData data = getTestData().get(0); reader.setInput(data.getInputStream()); @@ -493,8 +469,61 @@ public abstract class ImageReaderAbstractTestCase { } assertNotNull("Image was null!", image); - assertEquals("Read image has wrong width: ", (double) data.getDimension(0).width / 5.0, image.getWidth(), 1.0); - assertEquals("Read image has wrong height: ", (double) data.getDimension(0).height / 5.0, image.getHeight(), 1.0); + assertEquals("Read image has wrong width: ", (data.getDimension(0).width + 4) / 5, image.getWidth()); + assertEquals("Read image has wrong height: ", (data.getDimension(0).height + 4) / 5, image.getHeight()); + } + + @Ignore + @Test + public void testReadWithSubsampleParamPixels() throws IOException { + ImageReader reader = createReader(); + TestData data = getTestData().get(0); + reader.setInput(data.getInputStream()); + + ImageReadParam param = reader.getDefaultReadParam(); + param.setSourceRegion(new Rectangle(Math.min(100, reader.getWidth(0)), Math.min(100, reader.getHeight(0)))); + + BufferedImage image = null; + BufferedImage subsampled = null; + try { + image = reader.read(0, param); + param.setSourceSubsampling(2, 2, 1, 1); // Hmm.. Seems to be the offset the fake version (ReplicateScaleFilter) uses + + subsampled = reader.read(0, param); + } + catch (IOException e) { + failBecause("Image could not be read", e); + } + + BufferedImage expected = ImageUtil.toBuffered(IIOUtil.fakeSubsampling(image, param)); + +// JPanel panel = new JPanel(); +// panel.add(new JLabel("Expected", new BufferedImageIcon(expected, 300, 300), JLabel.CENTER)); +// panel.add(new JLabel("Actual", new BufferedImageIcon(subsampled, 300, 300), JLabel.CENTER)); +// JOptionPane.showConfirmDialog(null, panel); + + assertImageDataEquals("Subsampled image data does not match expected", expected, subsampled); + } + + protected final void assertImageDataEquals(String message, BufferedImage expected, BufferedImage actual) { + assertNotNull("Expected image was null", expected); + assertNotNull("Actual image was null!", actual); + + if (expected == actual) { + return; + } + + for (int y = 0; y < expected.getHeight(); y++) { + for (int x = 0; x < expected.getWidth(); x++) { + int expectedRGB = expected.getRGB(x, y); + int actualRGB = actual.getRGB(x, y); + + assertEquals(String.format("%s alpha at (%d, %d)", message, x, y), (expectedRGB >> 24) & 0xff, (actualRGB >> 24) & 0xff, 5); + assertEquals(String.format("%s red at (%d, %d)", message, x, y), (expectedRGB >> 16) & 0xff, (actualRGB >> 16) & 0xff, 5); + assertEquals(String.format("%s green at (%d, %d)", message, x, y), (expectedRGB >> 8) & 0xff, (actualRGB >> 8) & 0xff, 5); + assertEquals(String.format("%s blue at (%d, %d)", message, x, y), expectedRGB & 0xff, actualRGB & 0xff, 5); + } + } } @Test @@ -513,6 +542,7 @@ public abstract class ImageReaderAbstractTestCase { catch (IOException e) { failBecause("Image could not be read", e); } + assertNotNull("Image was null!", image); assertEquals("Read image has wrong width: " + image.getWidth(), 10, image.getWidth()); assertEquals("Read image has wrong height: " + image.getHeight(), 10, image.getHeight()); @@ -540,6 +570,7 @@ public abstract class ImageReaderAbstractTestCase { catch (IOException e) { failBecause("Image could not be read", e); } + assertNotNull("Image was null!", image); assertEquals("Read image has wrong width: " + image.getWidth(), 10, image.getWidth()); assertEquals("Read image has wrong height: " + image.getHeight(), 10, image.getHeight()); @@ -1352,7 +1383,7 @@ public abstract class ImageReaderAbstractTestCase { boolean removed = illegalTypes.remove(valid); // TODO: 4BYTE_ABGR (6) and 4BYTE_ABGR_PRE (7) is essentially the same type... - // !#$#ďż˝%$! ImageTypeSpecifier.equals is not well-defined + // #$@*%$! ImageTypeSpecifier.equals is not well-defined if (!removed) { for (Iterator iterator = illegalTypes.iterator(); iterator.hasNext();) { ImageTypeSpecifier illegalType = iterator.next(); diff --git a/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java b/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java index 527a0ba0..bd8acd6a 100644 --- a/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java +++ b/imageio/imageio-icns/src/test/java/com/twelvemonkeys/imageio/plugins/icns/ICNSImageReaderTest.java @@ -29,10 +29,13 @@ package com.twelvemonkeys.imageio.plugins.icns; import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; +import org.junit.Ignore; +import org.junit.Test; import javax.imageio.ImageReader; import javax.imageio.spi.ImageReaderSpi; import java.awt.*; +import java.io.IOException; import java.util.Arrays; import java.util.List; @@ -61,7 +64,7 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase { new Dimension(32, 32), // 24 bit + 8 bit mask new Dimension(48, 48), // 24 bit + 8 bit mask new Dimension(128, 128), // 24 bit + 8 bit mask - new Dimension(256, 256), // JPEG 2000 ic08 + new Dimension(256, 256), // JPEG 2000 ic08 new Dimension(512, 512) // JPEG 2000 ic09 ), new TestData( @@ -69,7 +72,7 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase { new Dimension(16, 16), // 24 bit + 8 bit mask new Dimension(32, 32), // 24 bit + 8 bit mask new Dimension(128, 128), // 24 bit + 8 bit mask - new Dimension(256, 256), // JPEG 2000 ic08 + new Dimension(256, 256), // JPEG 2000 ic08 new Dimension(512, 512) // JPEG 2000 ic09 ), new TestData( @@ -128,4 +131,11 @@ public class ICNSImageReaderTest extends ImageReaderAbstractTestCase { protected List getMIMETypes() { return Arrays.asList("image/x-apple-icons"); } + + @Test + @Ignore("Known issue: Subsampled reading not supported") + @Override + public void testReadWithSubsampleParamPixels() throws IOException { + super.testReadWithSubsampleParamPixels(); + } } diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java index 53f2fc8c..227f1199 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java @@ -613,12 +613,12 @@ public class IFFImageReader extends ImageReaderBase { } // Skip rows outside AOI - if (srcY < aoi.y || (srcY - aoi.y) % sourceYSubsampling != 0) { - continue; - } - else if (srcY >= (aoi.y + aoi.height)) { + if (srcY >= (aoi.y + aoi.height)) { return; } + else if (srcY < aoi.y || (srcY - aoi.y) % sourceYSubsampling != 0) { + continue; + } if (formType == IFF.TYPE_ILBM) { // NOTE: Using (channels - c - 1) instead of just c, @@ -639,19 +639,21 @@ public class IFFImageReader extends ImageReaderBase { } } - int dstY = (srcY - aoi.y) / sourceYSubsampling; - // TODO: Support conversion to INT (A)RGB rasters (maybe using ColorConvertOp?) - // TODO: Avoid createChild if no region? - if (sourceXSubsampling == 1) { - destination.setRect(0, dstY, sourceRow); + if (srcY >= aoi.y && (srcY - aoi.y) % sourceYSubsampling == 0) { + int dstY = (srcY - aoi.y) / sourceYSubsampling; + // TODO: Support conversion to INT (A)RGB rasters (maybe using ColorConvertOp?) + // TODO: Avoid createChild if no region? + if (sourceXSubsampling == 1) { + destination.setRect(0, dstY, sourceRow); // dataElements = raster.getDataElements(aoi.x, 0, aoi.width, 1, dataElements); // destination.setDataElements(offset.x, offset.y + (srcY - aoi.y) / sourceYSubsampling, aoi.width, 1, dataElements); - } - else { - for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) { - dataElements = sourceRow.getDataElements(srcX, 0, dataElements); - int dstX = srcX / sourceXSubsampling; - destination.setDataElements(dstX, dstY, dataElements); + } + else { + for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) { + dataElements = sourceRow.getDataElements(srcX, 0, dataElements); + int dstX = srcX / sourceXSubsampling; + destination.setDataElements(dstX, dstY, dataElements); + } } } From bf1aae66520fb4a409bc7d702945be4cd86bb25c Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Fri, 25 Oct 2013 13:05:36 +0200 Subject: [PATCH 29/98] Removed Maven warnings due to missing encoding/missing depency/plugin versions. Build should now be stable. --- pom.xml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 7a805421..6cf590a1 100755 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,10 @@ + + UTF-8 + + twelvemonkeys-${project.artifactId}-${project.version} @@ -64,6 +68,7 @@ org.apache.maven.plugins maven-resources-plugin + 2.5 UTF-8 @@ -71,7 +76,7 @@ org.apache.maven.plugins maven-jar-plugin - 2.2 + 2.4 true @@ -86,6 +91,7 @@ org.apache.maven.plugins maven-source-plugin + 2.2.1 true @@ -102,11 +108,13 @@ org.apache.maven.plugins maven-compiler-plugin + 2.3.2 true 1.5 1.5 - true + false + -g:lines iso-8859-1 @@ -116,6 +124,7 @@ org.apache.maven.plugins maven-surefire-plugin + 2.10 @@ -156,18 +165,22 @@ org.apache.maven.plugins maven-javadoc-plugin + 2.9.1 org.apache.maven.plugins maven-surefire-report-plugin + 2.16 org.codehaus.mojo cobertura-maven-plugin + 2.6 org.apache.maven.plugins maven-pmd-plugin + 3.0.1 1.5 @@ -175,6 +188,7 @@ org.apache.maven.plugins maven-checkstyle-plugin + 2.10 From ae58b859e4115393ae3934b3aa916df04b4ce06c Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Fri, 25 Oct 2013 17:09:20 +0200 Subject: [PATCH 30/98] TMI-JPEG-4: Fixed issue related to X/Y density out of range. --- .../jpeg/JPEGImage10MetadataCleaner.java | 4 ++-- .../plugins/jpeg/JPEGImageReaderTest.java | 11 ++++++++++- .../jpeg/xdensity-out-of-range-zero.jpg | Bin 0 -> 233579 bytes 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 imageio/imageio-jpeg/src/test/resources/jpeg/xdensity-out-of-range-zero.jpg diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java index 2082eaf0..292a6bc8 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java @@ -88,8 +88,8 @@ final class JPEGImage10MetadataCleaner { jfif.setAttribute("majorVersion", String.valueOf(jfifSegment.majorVersion)); jfif.setAttribute("minorVersion", String.valueOf(jfifSegment.minorVersion)); jfif.setAttribute("resUnits", String.valueOf(jfifSegment.units)); - jfif.setAttribute("Xdensity", String.valueOf(jfifSegment.xDensity)); - jfif.setAttribute("Ydensity", String.valueOf(jfifSegment.yDensity)); + jfif.setAttribute("Xdensity", String.valueOf(Math.max(1, jfifSegment.xDensity))); // Avoid 0 density + jfif.setAttribute("Ydensity", String.valueOf(Math.max(1,jfifSegment.yDensity))); jfif.setAttribute("thumbWidth", String.valueOf(jfifSegment.xThumbnail)); jfif.setAttribute("thumbHeight", String.valueOf(jfifSegment.yThumbnail)); diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java index 6666d9ac..fbeffd60 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java @@ -55,7 +55,6 @@ import java.util.*; import java.util.List; import static org.junit.Assert.*; -import static org.junit.Assert.assertArrayEquals; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; @@ -639,6 +638,16 @@ public class JPEGImageReaderTest extends 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%0A4BabwZ1VP|W+ihaaGdWY`iYozT^@fdv^S0ZQ}Q~ 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 z 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 f(`?zx8+6Gz Date: Wed, 30 Oct 2013 13:49:23 +0100 Subject: [PATCH 31/98] TMI-JPEG-4: Code clean up --- .../twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java index 828056f9..704dc1c9 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java @@ -130,8 +130,10 @@ final class EXIFThumbnailReader extends ThumbnailReader { }; input = new SequenceInputStream(new ByteArrayInputStream(fakeEmptyExif), input); + try { MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(input); + try { return readJPEGThumbnail(reader, stream); } From f83ca01e8f217449a9221c8571d9abe9338c420c Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 17:03:10 +0100 Subject: [PATCH 32/98] Finally a more useful readme. --- README.md | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..d83d6663 --- /dev/null +++ b/README.md @@ -0,0 +1,173 @@ +We did it + +----- + +## Background + +TwelveMonkeys ImageIO is a collection of plug-ins for Java's ImageIO. + + +These plugins extends the number of file formats supported in Java, using the javax.imageio.* package. +The main purpose of this project is to provide support for formats not covered by the JDK itself. + +Support for formats is important, both to be able to read data found +"in the wild", as well as to maintain access to data in legacy formats. +Because there is lots of legacy data out there, we see the need for open implementations of readers for popular formats. +The goal is to create a set of efficient and robust ImageIO plug-ins, that can be distributed independently. + + + +## Features + +Mainstream format support + +JPEG + full EXIF support + support for CMYK/YCCK + + +JPEG-LS + possibly coming in the future + +JPEG-2000 + possibly coming in the future, pending some license issues + +PSD + read-only support + +TIFF + read-only support (for now) + +PICT + Legacy format, especially useful for reading OS X clipboard data. + read and limited write support + +IFF + Legacy format, allows reading popular image from the Commodore Amiga computer. + read and write support + + +Icon/other formats + +ICNS +ICO +Thumbs.db + + +Other formats, using 3rd party libraries +SVG + read-only support using Batik +WMF + limited read-only support using Batik + + +Other useful stuff in the core package? + + + +## Usage + +Most of the time, all you need to do is simply: + + BufferedImage image = ImageIO.read(file); + +For more advanced usage, and information on how to use the ImageIO API, I suggest you read the +[Java Image I/O API Guide](http://docs.oracle.com/javase/6/docs/technotes/guides/imageio/spec/imageio_guideTOC.fm.html) +from Oracle. + + + + ResampleOp + + + +## Building + + $ mvn clean install + + +## Installing + +To install the plug-ins, +Either use Maven and add the necessary dependencies to your project, +or manually add the needed JARs along with required dependencies in class-path. + +The ImageIO registry and service lookup mechanism will make sure the plugins are available for use. + +To verify that the plugin is installed and used at run-time, you could use the following code: + + Iterator readers = ImageIO.getImageReadersByFormatName("JPEG"); + while (readers.hasNext()) { + System.out.println("reader: " + readers.next()); + } + +The first line should print: + + reader: com.twelvemonkeys.imageio.jpeg.JPEGImageReader@somehash + +TODO: Maven dependency example + +TODO: Manual dependency with hierarchy + +TODO: Links to prebuilt binaries + + + +## FAQ + +q: How do I use it? + +a: The easiest way is to build your own project using Maven, and just add dependencies to the specific plug-ins you need. + If you don't use Maven, make sure you have all the necessary JARs in classpath. See the Install section below. + + +q: What changes do I have to make to my code in order to use the plug-ins? + +a: The short answer is: None. For basic usage, like ImageIO.read(...) or ImageIO.getImageReaders(...), there is no need +to change your code. Most of the functionality is available through standard ImageIO APIs, and great care has been taken + not to introduce extra API where none is necessary. + +Should you want to use very specific/advanced features of some of the formats, you might have to use specific APIs, like + setting base URL for an SVG image that consists of multiple files, + or controlling the output compression of a TIFF file. + + +q: How does it work? + +a: The TwelveMonkeys ImageIO project contains plug-ins for ImageIO. + +ImageIO uses a service lookup mechanism, to discover plug-ins at runtime. + +TODO: Describe SPI mechanism. + +All you have have to do, is to make sure you have the TwelveMonkeys JARs in your classpath. + +The fine print: The TwelveMonkeys service providers for TIFF and JPEG overrides the onRegistration method, and +utilizes the pairwise partial ordering mechanism of the IIOServiceRegistry to make sure it is installed before +the Sun/Oracle provided JPEGImageReader and the Apple provided TIFFImageReader on OS X, respectively. +Using the pairwise ordering will not remove any functionality form these implementations, but in most cases you'll end +up using the TwelveMonkeys plug-ins instead. + + +q: What about JAI? Several of the formats are already supported by JAI. + +a: While JAI (and jai-imageio in particular) have support for some of the formats, JAI has some major issues. +The most obvious being: +- It's not actively developed. No issues has been fixed for years. +- To get full format support, you need native libs. +Native libs does not exist for several popular platforms/architectures, and further the native libs are not open source. +Some environments may also prevent deployment of native libs, which brings us back to square one. + + +q: What about JMagick or IM4Java? Can't you just use what´s already available? + +a: While great libraries with a wide range of formats support, the ImageMagick-based libraries has some disadvantages +compared to ImageIO. +- No real stream support, these libraries only work with files. +- No easy access to pixel data through standard Java2D/BufferedImage API. +- Not a pure Java solution, requires system specific native libs. + + +----- + +We did it From ef13030cc73a433fa232893b2abfdb29147de9a0 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 17:11:07 +0100 Subject: [PATCH 33/98] Updated readme. --- README.md | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d83d6663..b5a5a231 100644 --- a/README.md +++ b/README.md @@ -21,48 +21,60 @@ The goal is to create a set of efficient and robust ImageIO plug-ins, that can b Mainstream format support -JPEG +# JPEG + full EXIF support support for CMYK/YCCK -JPEG-LS +# JPEG-LS + possibly coming in the future -JPEG-2000 +# JPEG-2000 + possibly coming in the future, pending some license issues -PSD +# PSD + read-only support -TIFF +# TIFF + read-only support (for now) -PICT +# PICT + Legacy format, especially useful for reading OS X clipboard data. read and limited write support -IFF +# IFF + Legacy format, allows reading popular image from the Commodore Amiga computer. read and write support Icon/other formats -ICNS -ICO -Thumbs.db +# ICNS + +# ICO + +# Thumbs.db Other formats, using 3rd party libraries -SVG + +# SVG + read-only support using Batik -WMF + +# WMF + limited read-only support using Batik -Other useful stuff in the core package? - +TODO: Docuemnt other useful stuff in the core package? ## Usage @@ -76,6 +88,7 @@ For more advanced usage, and information on how to use the ImageIO API, I sugges from Oracle. +TODO: Docuemnt ResampleOp as well? ResampleOp @@ -112,7 +125,6 @@ TODO: Manual dependency with hierarchy TODO: Links to prebuilt binaries - ## FAQ q: How do I use it? From 7e14f0b37c10cafd7d53a51c75f215f973a30f05 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 17:12:31 +0100 Subject: [PATCH 34/98] updated readme. --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b5a5a231..82201247 100644 --- a/README.md +++ b/README.md @@ -21,34 +21,34 @@ The goal is to create a set of efficient and robust ImageIO plug-ins, that can b Mainstream format support -# JPEG +#### JPEG full EXIF support support for CMYK/YCCK -# JPEG-LS +#### JPEG-LS possibly coming in the future -# JPEG-2000 +#### JPEG-2000 possibly coming in the future, pending some license issues -# PSD +#### PSD read-only support -# TIFF +#### TIFF read-only support (for now) -# PICT +#### PICT Legacy format, especially useful for reading OS X clipboard data. read and limited write support -# IFF +#### IFF Legacy format, allows reading popular image from the Commodore Amiga computer. read and write support @@ -56,20 +56,20 @@ Mainstream format support Icon/other formats -# ICNS +#### ICNS -# ICO +#### ICO -# Thumbs.db +#### Thumbs.db Other formats, using 3rd party libraries -# SVG +#### SVG read-only support using Batik -# WMF +#### WMF limited read-only support using Batik From 5064d08dd6dd89b81aafaea730932272c4a7474d Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 17:16:00 +0100 Subject: [PATCH 35/98] updated readme. --- README.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 82201247..f6731f3b 100644 --- a/README.md +++ b/README.md @@ -23,35 +23,36 @@ Mainstream format support #### JPEG - full EXIF support - support for CMYK/YCCK - +* Full EXIF support +* Support for CMYK/YCCK +* Support for non-JFIF YCbCr data #### JPEG-LS - possibly coming in the future +* Possibly coming in the future? #### JPEG-2000 - possibly coming in the future, pending some license issues +* Possibly coming in the future, pending some license issues #### PSD - read-only support +* Read-only support #### TIFF - read-only support (for now) +* Read-only support (for now) +* Write support in progress #### PICT - Legacy format, especially useful for reading OS X clipboard data. - read and limited write support +* Legacy format, especially useful for reading OS X clipboard data. +* Read and limited write support #### IFF - Legacy format, allows reading popular image from the Commodore Amiga computer. - read and write support +* Legacy format, allows reading popular image from the Commodore Amiga computer. +* Read and write support Icon/other formats @@ -67,11 +68,11 @@ Other formats, using 3rd party libraries #### SVG - read-only support using Batik +* Read-only support using Batik #### WMF - limited read-only support using Batik +* Limited read-only support using Batik TODO: Docuemnt other useful stuff in the core package? From cb82b39e6a0465d0b31e3d0096ff920f461dec0c Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 17:16:20 +0100 Subject: [PATCH 36/98] Delete old README. --- README | 1 - 1 file changed, 1 deletion(-) delete mode 100644 README diff --git a/README b/README deleted file mode 100644 index cb72f39a..00000000 --- a/README +++ /dev/null @@ -1 +0,0 @@ -We did it \ No newline at end of file From dc313225185c1312363aff3c93b29e4748de3bb9 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 17:32:59 +0100 Subject: [PATCH 37/98] Updated readme. --- README.md | 71 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f6731f3b..3c53329a 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,12 @@ Mainstream format support #### JPEG -* Full EXIF support -* Support for CMYK/YCCK +* EXIF support +* Support for CMYK/YCCK data * Support for non-JFIF YCbCr data - -#### JPEG-LS - -* Possibly coming in the future? +* Thumbnail support (JFIF, JFXX and EXIF) +* Extended metadata support +* Extended write support in progress #### JPEG-2000 @@ -37,32 +36,88 @@ Mainstream format support #### PSD -* Read-only support +* Adobe +* Read-only support for the following file types: +** Monochrome, 1 channel, 1 bit +** Indexed, 1 channel, 8 bit +** Gray, 1 channel, 8 and 16 bit +** Duotone, 1 channel, 8 and 16 bit +** RGB, 3-4 channels, 8 and 16 bit +** CMYK, 4-5 channels, 8 and 16 bit +* Read support for the following compression types: +** Uncompressed +** RLE (PackBits) #### TIFF * Read-only support (for now) * Write support in progress +* Read support for the following "Baseline" TIFF file types: +** Class B (Bi-level), all relevant compression types, 1 bit per sample< +** Class G (Gray), all relevant compression types, 2, 4, 8, 16 or 32 bits per sample, unsigned integer +** Class P (Palette/indexed color), all relevant compression types, 1, 2, 4, 8 or 16 bits per sample, unsigned integer +** Class R (RGB), all relevant compression types, 8 or 16 bits per sample, unsigned integer +* Read support for the following TIFF extensions: +** Tiling +** LZW Compression (type 5) +** "Old-style" JPEG Compression (type 6), as a best effort, as the spec is not well-defined +** JPEG Compression (type 7) +** ZLib (aka Adobe-style Deflate) Compression (type 8) +** Deflate Compression (type 32946) +** Horizontal differencing Predictor (type 2) for LZW, ZLib, Deflate and PackBits compression +** Alpha channel (ExtraSamples type 1/Associated Alpha) +** CMYK data (PhotometricInterpretation type 5/Separated) +** YCbCr data (PhotometricInterpretation type 6/YCbCr) for JPEG +** Planar data (PlanarConfiguration type 2/Planar) +** ICC profiles (ICCProfile) +** BitsPerSample values up to 16 for most PhotometricInterpretations +** Multiple images (pages) in one file #### PICT * Legacy format, especially useful for reading OS X clipboard data. * Read and limited write support +* Read support for the following file types: +** QuickDraw (format support is not complete, but supports most OS X clipboard data as well as RGB pixel data) +** QuickDraw bitmap +** QuickDraw pixmap +** QuickTime stills +* Writing is limited to RGB pixel data #### IFF * Legacy format, allows reading popular image from the Commodore Amiga computer. * Read and write support - +* Read support for the following file types: +** ILBM Indexed color, 1-8 interleaved bit planes, including 6 bit EHB +** ILBM Gray, 8 bit interleaved bit planes +** ILBM RGB, 24 and 32 bit interleaved bit planes +** ILBM HAM6 and HAM8 +** PBM Indexed color, 1-8 bit, +** PBM Gray, 8 bit +** PBM RGB, 24 and 32 bit +** PBM HAM6 and HAM8 +* Support for the following compression types: +** Uncompressed +** RLE (PackBits) Icon/other formats #### ICNS +* Read support for most icon types, including PNG and JPEG 2000 (requires JPEG 2000 ImageIO plugin) + #### ICO +* Read support for the following file types: +** ICO Indexed color, 1, 4 and 8 bit +** ICO RGB, 16, 24 and 32 bit +** CUR Indexed color, 1, 4 and 8 bit +** CUR RGB, 16, 24 and 32 bit + #### Thumbs.db +* Read support Other formats, using 3rd party libraries From 9cab7903eda42e2ad98e6df4714f39f8b79df7be Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 17:34:43 +0100 Subject: [PATCH 38/98] Updated readme. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3c53329a..019acbfd 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Mainstream format support * Adobe * Read-only support for the following file types: -** Monochrome, 1 channel, 1 bit + * Monochrome, 1 channel, 1 bit ** Indexed, 1 channel, 8 bit ** Gray, 1 channel, 8 and 16 bit ** Duotone, 1 channel, 8 and 16 bit @@ -53,7 +53,7 @@ Mainstream format support * Read-only support (for now) * Write support in progress * Read support for the following "Baseline" TIFF file types: -** Class B (Bi-level), all relevant compression types, 1 bit per sample< + * Class B (Bi-level), all relevant compression types, 1 bit per sample ** Class G (Gray), all relevant compression types, 2, 4, 8, 16 or 32 bits per sample, unsigned integer ** Class P (Palette/indexed color), all relevant compression types, 1, 2, 4, 8 or 16 bits per sample, unsigned integer ** Class R (RGB), all relevant compression types, 8 or 16 bits per sample, unsigned integer From 5189c7c1e709d7c2a6c5bae9afc6a7d745093dff Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 17:35:34 +0100 Subject: [PATCH 39/98] Updated readme with correct bulleting. --- README.md | 84 +++++++++++++++++++++++++++---------------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 019acbfd..c2e8a533 100644 --- a/README.md +++ b/README.md @@ -39,14 +39,14 @@ Mainstream format support * Adobe * Read-only support for the following file types: * Monochrome, 1 channel, 1 bit -** Indexed, 1 channel, 8 bit -** Gray, 1 channel, 8 and 16 bit -** Duotone, 1 channel, 8 and 16 bit -** RGB, 3-4 channels, 8 and 16 bit -** CMYK, 4-5 channels, 8 and 16 bit + * Indexed, 1 channel, 8 bit + * Gray, 1 channel, 8 and 16 bit + * Duotone, 1 channel, 8 and 16 bit + * RGB, 3-4 channels, 8 and 16 bit + * CMYK, 4-5 channels, 8 and 16 bit * Read support for the following compression types: -** Uncompressed -** RLE (PackBits) + * Uncompressed + * RLE (PackBits) #### TIFF @@ -54,34 +54,34 @@ Mainstream format support * Write support in progress * Read support for the following "Baseline" TIFF file types: * Class B (Bi-level), all relevant compression types, 1 bit per sample -** Class G (Gray), all relevant compression types, 2, 4, 8, 16 or 32 bits per sample, unsigned integer -** Class P (Palette/indexed color), all relevant compression types, 1, 2, 4, 8 or 16 bits per sample, unsigned integer -** Class R (RGB), all relevant compression types, 8 or 16 bits per sample, unsigned integer + * Class G (Gray), all relevant compression types, 2, 4, 8, 16 or 32 bits per sample, unsigned integer + * Class P (Palette/indexed color), all relevant compression types, 1, 2, 4, 8 or 16 bits per sample, unsigned integer + * Class R (RGB), all relevant compression types, 8 or 16 bits per sample, unsigned integer * Read support for the following TIFF extensions: -** Tiling -** LZW Compression (type 5) -** "Old-style" JPEG Compression (type 6), as a best effort, as the spec is not well-defined -** JPEG Compression (type 7) -** ZLib (aka Adobe-style Deflate) Compression (type 8) -** Deflate Compression (type 32946) -** Horizontal differencing Predictor (type 2) for LZW, ZLib, Deflate and PackBits compression -** Alpha channel (ExtraSamples type 1/Associated Alpha) -** CMYK data (PhotometricInterpretation type 5/Separated) -** YCbCr data (PhotometricInterpretation type 6/YCbCr) for JPEG -** Planar data (PlanarConfiguration type 2/Planar) -** ICC profiles (ICCProfile) -** BitsPerSample values up to 16 for most PhotometricInterpretations -** Multiple images (pages) in one file + * Tiling + * LZW Compression (type 5) + * "Old-style" JPEG Compression (type 6), as a best effort, as the spec is not well-defined + * JPEG Compression (type 7) + * ZLib (aka Adobe-style Deflate) Compression (type 8) + * Deflate Compression (type 32946) + * Horizontal differencing Predictor (type 2) for LZW, ZLib, Deflate and PackBits compression + * Alpha channel (ExtraSamples type 1/Associated Alpha) + * CMYK data (PhotometricInterpretation type 5/Separated) + * YCbCr data (PhotometricInterpretation type 6/YCbCr) for JPEG + * Planar data (PlanarConfiguration type 2/Planar) + * ICC profiles (ICCProfile) + * BitsPerSample values up to 16 for most PhotometricInterpretations + * Multiple images (pages) in one file #### PICT * Legacy format, especially useful for reading OS X clipboard data. * Read and limited write support * Read support for the following file types: -** QuickDraw (format support is not complete, but supports most OS X clipboard data as well as RGB pixel data) -** QuickDraw bitmap -** QuickDraw pixmap -** QuickTime stills + * QuickDraw (format support is not complete, but supports most OS X clipboard data as well as RGB pixel data) + * QuickDraw bitmap + * QuickDraw pixmap + * QuickTime stills * Writing is limited to RGB pixel data #### IFF @@ -89,17 +89,17 @@ Mainstream format support * Legacy format, allows reading popular image from the Commodore Amiga computer. * Read and write support * Read support for the following file types: -** ILBM Indexed color, 1-8 interleaved bit planes, including 6 bit EHB -** ILBM Gray, 8 bit interleaved bit planes -** ILBM RGB, 24 and 32 bit interleaved bit planes -** ILBM HAM6 and HAM8 -** PBM Indexed color, 1-8 bit, -** PBM Gray, 8 bit -** PBM RGB, 24 and 32 bit -** PBM HAM6 and HAM8 + * ILBM Indexed color, 1-8 interleaved bit planes, including 6 bit EHB + * ILBM Gray, 8 bit interleaved bit planes + * ILBM RGB, 24 and 32 bit interleaved bit planes + * ILBM HAM6 and HAM8 + * PBM Indexed color, 1-8 bit, + * PBM Gray, 8 bit + * PBM RGB, 24 and 32 bit + * PBM HAM6 and HAM8 * Support for the following compression types: -** Uncompressed -** RLE (PackBits) + * Uncompressed + * RLE (PackBits) Icon/other formats @@ -110,10 +110,10 @@ Icon/other formats #### ICO * Read support for the following file types: -** ICO Indexed color, 1, 4 and 8 bit -** ICO RGB, 16, 24 and 32 bit -** CUR Indexed color, 1, 4 and 8 bit -** CUR RGB, 16, 24 and 32 bit + * ICO Indexed color, 1, 4 and 8 bit + * ICO RGB, 16, 24 and 32 bit + * CUR Indexed color, 1, 4 and 8 bit + * CUR RGB, 16, 24 and 32 bit #### Thumbs.db From e2d56659ca73df055f5e46e388f0f21044f2a167 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 17:36:40 +0100 Subject: [PATCH 40/98] Updated readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c2e8a533..e094e72c 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ TODO: Links to prebuilt binaries q: How do I use it? a: The easiest way is to build your own project using Maven, and just add dependencies to the specific plug-ins you need. - If you don't use Maven, make sure you have all the necessary JARs in classpath. See the Install section below. + If you don't use Maven, make sure you have all the necessary JARs in classpath. See the Install section above. q: What changes do I have to make to my code in order to use the plug-ins? From c3ee44992a98cc6e414e6688647b4ddeee004aee Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 17:39:25 +0100 Subject: [PATCH 41/98] Updated readme. --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index e094e72c..d25b3a51 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ -We did it - ------ - ## Background TwelveMonkeys ImageIO is a collection of plug-ins for Java's ImageIO. @@ -15,7 +11,7 @@ Support for formats is important, both to be able to read data found Because there is lots of legacy data out there, we see the need for open implementations of readers for popular formats. The goal is to create a set of efficient and robust ImageIO plug-ins, that can be distributed independently. - +---- ## Features From cc44e73d7dd612af0388a03b6adeb99be06fd81a Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 17:44:31 +0100 Subject: [PATCH 42/98] Updated readme. --- README.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d25b3a51..e7ee18a7 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,10 @@ Mainstream format support #### JPEG -* EXIF support +* Exif support * Support for CMYK/YCCK data * Support for non-JFIF YCbCr data -* Thumbnail support (JFIF, JFXX and EXIF) +* Thumbnail support (JFIF, JFXX and Exif) * Extended metadata support * Extended write support in progress @@ -30,9 +30,8 @@ Mainstream format support * Possibly coming in the future, pending some license issues -#### PSD +#### Adobe Photoshop Document (PSD) -* Adobe * Read-only support for the following file types: * Monochrome, 1 channel, 1 bit * Indexed, 1 channel, 8 bit @@ -44,7 +43,7 @@ Mainstream format support * Uncompressed * RLE (PackBits) -#### TIFF +#### Adobe (Aldus) Tagged Image File Format (TIFF) * Read-only support (for now) * Write support in progress @@ -69,7 +68,7 @@ Mainstream format support * BitsPerSample values up to 16 for most PhotometricInterpretations * Multiple images (pages) in one file -#### PICT +#### Apple Mac Paint Picture Format (PICT) * Legacy format, especially useful for reading OS X clipboard data. * Read and limited write support @@ -80,7 +79,7 @@ Mainstream format support * QuickTime stills * Writing is limited to RGB pixel data -#### IFF +#### Amiga/Electronic Arts Interchange File Format (IFF) * Legacy format, allows reading popular image from the Commodore Amiga computer. * Read and write support @@ -99,11 +98,11 @@ Mainstream format support Icon/other formats -#### ICNS +#### Apple Icon Image (ICNS= * Read support for most icon types, including PNG and JPEG 2000 (requires JPEG 2000 ImageIO plugin) -#### ICO +#### MS Windows Icon and Cursor Formats (ICO & CUR) * Read support for the following file types: * ICO Indexed color, 1, 4 and 8 bit @@ -111,7 +110,7 @@ Icon/other formats * CUR Indexed color, 1, 4 and 8 bit * CUR RGB, 16, 24 and 32 bit -#### Thumbs.db +#### MS Windows Thumbs DB (Thumbs.db) * Read support From 3bd8900cbea5aea8114a2adf6add4bc567aa6ec0 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 17:47:10 +0100 Subject: [PATCH 43/98] Updated readme. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e7ee18a7..21701fc7 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Mainstream format support * Uncompressed * RLE (PackBits) -#### Adobe (Aldus) Tagged Image File Format (TIFF) +#### Aldus/Adobe Tagged Image File Format (TIFF) * Read-only support (for now) * Write support in progress @@ -79,7 +79,7 @@ Mainstream format support * QuickTime stills * Writing is limited to RGB pixel data -#### Amiga/Electronic Arts Interchange File Format (IFF) +#### Commodore Amiga/Electronic Arts Interchange File Format (IFF) * Legacy format, allows reading popular image from the Commodore Amiga computer. * Read and write support @@ -98,7 +98,7 @@ Mainstream format support Icon/other formats -#### Apple Icon Image (ICNS= +#### Apple Icon Image (ICNS) * Read support for most icon types, including PNG and JPEG 2000 (requires JPEG 2000 ImageIO plugin) From 22d7ce80b1b830e2b310f01381490303c039c261 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 17:50:08 +0100 Subject: [PATCH 44/98] Updated readme. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 21701fc7..040c31f0 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,10 @@ Icon/other formats #### Apple Icon Image (ICNS) -* Read support for most icon types, including PNG and JPEG 2000 (requires JPEG 2000 ImageIO plugin) +* Read support for the following icon types: + * all known "native" icon types + * Large PNG encoded icons + * Large JPEG 2000 encoded icons (requires JPEG 2000 ImageIO plugin) #### MS Windows Icon and Cursor Formats (ICO & CUR) From 8f33f906fbe4b053ffb334e5439f1e504f8bce47 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 17:55:04 +0100 Subject: [PATCH 45/98] Updated readme. --- README.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 040c31f0..9e08fde8 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,26 @@ Mainstream format support #### JPEG -* Exif support -* Support for CMYK/YCCK data -* Support for non-JFIF YCbCr data -* Thumbnail support (JFIF, JFXX and Exif) -* Extended metadata support +* Read support for the following JPEG flavors: + * YCbCr JPEGs without JFIF segment (converted to RGB, using the embedded ICC profile if applicable) + * CMYK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable) + * Adobe YCCK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable) + * JPEGs containing ICC profiles with interpretation other than 'Perceptual' (profile is assumed to be 'Perceptual' and used) + * JPEGs containing ICC profiles with class other than 'Display' (profile is assumed to have class 'Display' and used) + * JPEGs containing ICC profiles that are incompatible with stream data (image data is read, profile is ignored) + * JPEGs with corrupted ICC profiles (image data is read, profile is ignored) + * JPEGs with corrupted {@code ICC_PROFILE} segments (image data is read, profile is ignored) + * JPEGs using non-standard color spaces, unsupported by Java 2D (image data is read, profile is ignored) + * Issues warnings instead of throwing exceptions in cases of corrupted data where ever the image data can still be read in a reasonable way +* Thumbnail support: + * JFIF thumbnails (even if stream contains inconsistent metadata) + * JFXX thumbnails (JPEG, Indexed and RGB) + * EXIF thumbnails (JPEG, RGB and YCbCr) +* Metadata support: + * JPEG metadata in both standard and native formats (even if stream contains inconsistent metadata) + * `javax_imageio_jpeg_image_1.0` format (currently as native format, may change in the future) + * illegal combinations of JFIF, Exif and Adobe markers, using "unknown" segments in the + * "MarkerSequence" tag for the unsupported segments (for `javax_imageio_jpeg_image_1.0` format) * Extended write support in progress #### JPEG-2000 From a36eb0cd5d71ab8ba6673dc2e64b1277a0b5ecc4 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 17:58:27 +0100 Subject: [PATCH 46/98] Updated readme. --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9e08fde8..7766a497 100644 --- a/README.md +++ b/README.md @@ -20,15 +20,15 @@ Mainstream format support #### JPEG * Read support for the following JPEG flavors: - * YCbCr JPEGs without JFIF segment (converted to RGB, using the embedded ICC profile if applicable) - * CMYK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable) - * Adobe YCCK JPEGs (converted to RGB by default or as CMYK, using the embedded ICC profile if applicable) - * JPEGs containing ICC profiles with interpretation other than 'Perceptual' (profile is assumed to be 'Perceptual' and used) - * JPEGs containing ICC profiles with class other than 'Display' (profile is assumed to have class 'Display' and used) - * JPEGs containing ICC profiles that are incompatible with stream data (image data is read, profile is ignored) - * JPEGs with corrupted ICC profiles (image data is read, profile is ignored) - * JPEGs with corrupted {@code ICC_PROFILE} segments (image data is read, profile is ignored) - * JPEGs using non-standard color spaces, unsupported by Java 2D (image data is read, profile is ignored) + * YCbCr JPEGs without JFIF segment (converted to RGB, using embedded ICC profile) + * CMYK JPEGs (converted to RGB by default or as CMYK, using embedded ICC profile ) + * Adobe YCCK JPEGs (converted to RGB by default or as CMYK, using embedded ICC profile) + * JPEGs containing ICC profiles with interpretation other than 'Perceptual' + * JPEGs containing ICC profiles with class other than 'Display' + * JPEGs containing ICC profiles that are incompatible with stream data + * JPEGs with corrupted ICC profiles + * JPEGs with corrupted `ICC_PROFILE` segments + * JPEGs using non-standard color spaces, unsupported by Java 2D * Issues warnings instead of throwing exceptions in cases of corrupted data where ever the image data can still be read in a reasonable way * Thumbnail support: * JFIF thumbnails (even if stream contains inconsistent metadata) @@ -37,8 +37,8 @@ Mainstream format support * Metadata support: * JPEG metadata in both standard and native formats (even if stream contains inconsistent metadata) * `javax_imageio_jpeg_image_1.0` format (currently as native format, may change in the future) - * illegal combinations of JFIF, Exif and Adobe markers, using "unknown" segments in the - * "MarkerSequence" tag for the unsupported segments (for `javax_imageio_jpeg_image_1.0` format) + * Illegal combinations of JFIF, Exif and Adobe markers, using "unknown" segments in the + "MarkerSequence" tag for the unsupported segments (for `javax_imageio_jpeg_image_1.0` format) * Extended write support in progress #### JPEG-2000 From 7527f2cdc6f3ad0f1800a953b52fbac6be5bcb03 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 18:00:25 +0100 Subject: [PATCH 47/98] Updated readme. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7766a497..8c46f626 100644 --- a/README.md +++ b/README.md @@ -134,11 +134,11 @@ Icon/other formats Other formats, using 3rd party libraries -#### SVG +#### Scalable Vector Graphics (SVG) * Read-only support using Batik -#### WMF +#### Windows MetaFile (WMF) * Limited read-only support using Batik From 973fe9fa37d6f5e447efb8a758a07ed48b542b6a Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 30 Oct 2013 18:02:41 +0100 Subject: [PATCH 48/98] Updated readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c46f626..3ee4932b 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ Other formats, using 3rd party libraries * Read-only support using Batik -#### Windows MetaFile (WMF) +#### MS Windows MetaFile (WMF) * Limited read-only support using Batik From 8aff3faa09f51e82edc8c6f1c28eb2ec39077718 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 31 Oct 2013 09:41:35 +0100 Subject: [PATCH 49/98] Updated readme. --- README.md | 90 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 3ee4932b..e3794f8d 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Mainstream format support #### Adobe Photoshop Document (PSD) -* Read-only support for the following file types: +* Read support for the following file types: * Monochrome, 1 channel, 1 bit * Indexed, 1 channel, 8 bit * Gray, 1 channel, 8 and 16 bit @@ -60,10 +60,8 @@ Mainstream format support #### Aldus/Adobe Tagged Image File Format (TIFF) -* Read-only support (for now) -* Write support in progress * Read support for the following "Baseline" TIFF file types: - * Class B (Bi-level), all relevant compression types, 1 bit per sample + * Class B (Bi-level), all relevant compression types, 1 bit per sample * Class G (Gray), all relevant compression types, 2, 4, 8, 16 or 32 bits per sample, unsigned integer * Class P (Palette/indexed color), all relevant compression types, 1, 2, 4, 8 or 16 bits per sample, unsigned integer * Class R (RGB), all relevant compression types, 8 or 16 bits per sample, unsigned integer @@ -82,22 +80,23 @@ Mainstream format support * ICC profiles (ICCProfile) * BitsPerSample values up to 16 for most PhotometricInterpretations * Multiple images (pages) in one file +* Write support in progress + * Will support writing most "Baseline" TIFF file types #### Apple Mac Paint Picture Format (PICT) * Legacy format, especially useful for reading OS X clipboard data. -* Read and limited write support * Read support for the following file types: * QuickDraw (format support is not complete, but supports most OS X clipboard data as well as RGB pixel data) * QuickDraw bitmap * QuickDraw pixmap * QuickTime stills -* Writing is limited to RGB pixel data +* Write support for RGB pixel data: + * QuickDraw pixmap #### Commodore Amiga/Electronic Arts Interchange File Format (IFF) * Legacy format, allows reading popular image from the Commodore Amiga computer. -* Read and write support * Read support for the following file types: * ILBM Indexed color, 1-8 interleaved bit planes, including 6 bit EHB * ILBM Gray, 8 bit interleaved bit planes @@ -107,7 +106,9 @@ Mainstream format support * PBM Gray, 8 bit * PBM RGB, 24 and 32 bit * PBM HAM6 and HAM8 -* Support for the following compression types: +* Write support + * ILBM Indexed color, 1-8 bits per sample, 8 bit gray, 24 and 32 bit true color. +* Support for the following compression types (read/write): * Uncompressed * RLE (PackBits) @@ -116,9 +117,9 @@ Icon/other formats #### Apple Icon Image (ICNS) * Read support for the following icon types: - * all known "native" icon types + * All known "native" icon types * Large PNG encoded icons - * Large JPEG 2000 encoded icons (requires JPEG 2000 ImageIO plugin) + * Large JPEG 2000 encoded icons (requires JPEG 2000 ImageIO plugin or fallback to `sips` command line tool) #### MS Windows Icon and Cursor Formats (ICO & CUR) @@ -143,9 +144,6 @@ Other formats, using 3rd party libraries * Limited read-only support using Batik -TODO: Docuemnt other useful stuff in the core package? - - ## Usage Most of the time, all you need to do is simply: @@ -157,26 +155,48 @@ For more advanced usage, and information on how to use the ImageIO API, I sugges from Oracle. -TODO: Docuemnt ResampleOp as well? +### Using the ResampleOp - ResampleOp +The library comes with a + import com.twelvemonkeys.image.ResampleOp; + + ... + + BufferedImage input = ...; // Image to resample + int width, height = ...; // new width/height + + BufferedImageOp resampler = new ResampleOp(width, height, ResampleOp.FILTER_LANCZOS); + BufferedImage output = resampler.filter(input, null); ## Building - $ mvn clean install +Download the project (using Git): + + $ git clone git@github.com:haraldk/TwelveMonkeys.git + +Build the project (using Maven): + + $ mvn package + +Because the unit tests needs quite a bit of memory to run, you might have to set the environment variable `MAVEN_OPTS` +to give the Java process that runs Maven more memory. I suggest something like `-Xmx512m -XX:MaxPermSize=256m`. + +Optionally install the project in your local Maven repository: + + $ mvn install ## Installing To install the plug-ins, -Either use Maven and add the necessary dependencies to your project, +either use Maven and add the necessary dependencies to your project, or manually add the needed JARs along with required dependencies in class-path. The ImageIO registry and service lookup mechanism will make sure the plugins are available for use. -To verify that the plugin is installed and used at run-time, you could use the following code: +To verify that the JPEG plugin is installed and used at run-time, you could use the following code: Iterator readers = ImageIO.getImageReadersByFormatName("JPEG"); while (readers.hasNext()) { @@ -187,12 +207,40 @@ The first line should print: reader: com.twelvemonkeys.imageio.jpeg.JPEGImageReader@somehash -TODO: Maven dependency example +#### Maven dependency example -TODO: Manual dependency with hierarchy +To depend on the JPEG and TIFF plugin using Maven, add the following to your POM: -TODO: Links to prebuilt binaries + ... + + ... + + com.twelvemonkeys.imageio + imageio-jpeg + 3.0-SNAPSHOT + + + com.twelvemonkeys.imageio + imageio-tiff + 3.0-SNAPSHOT + + +#### Manual dependency example + +To depend on the JPEG and TIFF plugin in your IDE or program, add all of the following JARs to your class path: + + twelvemonkeys-common-lang-3.0-SNAPSHOT.jar + twelvemonkeys-common-io-3.0-SNAPSHOT.jar + twelvemonkeys-common-image-3.0-SNAPSHOT.jar + twelvemonkeys-imageio-core-3.0-SNAPSHOT.jar + twelvemonkeys-imageio-metadata-3.0-SNAPSHOT.jar + twelvemonkeys-imageio-jpeg-3.0-SNAPSHOT.jar + twelvemonkeys-imageio-tiff-3.0-SNAPSHOT.jar + +### Links to prebuilt binaries + +There's no prebuilt binaries yet. ## FAQ From 3be5c1713b28d8cf59ab3008f86d484d8a2d9666 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 31 Oct 2013 09:49:58 +0100 Subject: [PATCH 50/98] Updated readme. --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e3794f8d..d338c222 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,8 @@ from Oracle. ### Using the ResampleOp -The library comes with a +The library comes with a resampling (image resizing) operation, that contains many different algorithms +to provide excellent results at reasonable speed. import com.twelvemonkeys.image.ResampleOp; @@ -166,9 +167,23 @@ The library comes with a BufferedImage input = ...; // Image to resample int width, height = ...; // new width/height - BufferedImageOp resampler = new ResampleOp(width, height, ResampleOp.FILTER_LANCZOS); + BufferedImageOp resampler = new ResampleOp(width, height, ResampleOp.FILTER_LANCZOS); // A good default filter, see class documentation for more info BufferedImage output = resampler.filter(input, null); +### Using the DiffusionDither + +The library comes with a dithering operation, that can be used to convert `BufferedImage`s to `IndexColorModel` using +Floyd-Steinberg error-diffusion dither. + + import com.twelvemonkeys.image.DiffusionDither; + + ... + + BufferedImage input = ...; // Image to dither + + BufferedImageOp ditherer = new DiffusionDither(); + BufferedImage output = ditherer.filter(input, null); + ## Building From 15cb7dd71bcf5138d39b1e4827556f8c246c3364 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 31 Oct 2013 10:01:19 +0100 Subject: [PATCH 51/98] Updated readme. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d338c222..5cdb77c4 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ For more advanced usage, and information on how to use the ImageIO API, I sugges from Oracle. -### Using the ResampleOp +#### Using the ResampleOp The library comes with a resampling (image resizing) operation, that contains many different algorithms to provide excellent results at reasonable speed. @@ -170,7 +170,7 @@ to provide excellent results at reasonable speed. BufferedImageOp resampler = new ResampleOp(width, height, ResampleOp.FILTER_LANCZOS); // A good default filter, see class documentation for more info BufferedImage output = resampler.filter(input, null); -### Using the DiffusionDither +#### Using the DiffusionDither The library comes with a dithering operation, that can be used to convert `BufferedImage`s to `IndexColorModel` using Floyd-Steinberg error-diffusion dither. From a5e634664774f4b0063198381c8dea74735d2002 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 31 Oct 2013 12:32:35 +0100 Subject: [PATCH 52/98] Documentation clean-up. --- .../twelvemonkeys/image/DiffusionDither.java | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/DiffusionDither.java b/common/common-image/src/main/java/com/twelvemonkeys/image/DiffusionDither.java index 48664fcc..d530d077 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/DiffusionDither.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/DiffusionDither.java @@ -17,7 +17,7 @@ import java.util.Random; * This {@code BufferedImageOp/RasterOp} implements basic * Floyd-Steinberg error-diffusion algorithm for dithering. *

- * The weights used are 7/16 3/16 5/16 1/16, distributed like this: + * The weights used are 7/16, 3/16, 5/16 and 1/16, distributed like this: *