diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedChannelImageInputStream.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedChannelImageInputStream.java
new file mode 100644
index 00000000..b11ef3ea
--- /dev/null
+++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedChannelImageInputStream.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (c) 2022, 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 of the copyright holder 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 HOLDER 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.imageio.stream;
+
+import javax.imageio.stream.ImageInputStreamImpl;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+import static com.twelvemonkeys.lang.Validate.notNull;
+import static java.lang.Math.max;
+
+/**
+ * A buffered {@link javax.imageio.stream.ImageInputStream} that is backed by a {@link java.nio.channels.SeekableByteChannel}
+ * and provides greatly improved performance
+ * compared to {@link javax.imageio.stream.FileCacheImageInputStream} or {@link javax.imageio.stream.MemoryCacheImageInputStream}
+ * for shorter reads, like single byte or bit reads.
+ */
+final class BufferedChannelImageInputStream extends ImageInputStreamImpl {
+ static final int DEFAULT_BUFFER_SIZE = 8192;
+
+ private ByteBuffer byteBuffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE);
+ private byte[] buffer = byteBuffer.array();
+ private int bufferPos;
+ private int bufferLimit;
+
+ private final ByteBuffer integralCache = ByteBuffer.allocate(8);
+ private final byte[] integralCacheArray = integralCache.array();
+
+ private SeekableByteChannel channel;
+
+ /**
+ * Constructs a {@code BufferedChannelImageInputStream} that will read from a given {@code File}.
+ *
+ * @param file a {@code File} to read from.
+ * @throws IllegalArgumentException if {@code file} is {@code null}.
+ * @throws SecurityException if a security manager is installed, and it denies read access to the file.
+ * @throws IOException if an I/O error occurs while opening the file.
+ */
+ public BufferedChannelImageInputStream(final File file) throws IOException {
+ this(notNull(file, "file").toPath());
+ }
+
+ /**
+ * Constructs a {@code BufferedChannelImageInputStream} that will read from a given {@code Path}.
+ *
+ * @param file a {@code Path} to read from.
+ * @throws IllegalArgumentException if {@code file} is {@code null}.
+ * @throws UnsupportedOperationException if the {@code file} is associated with a provider that does not support creating file channels.
+ * @throws IOException if an I/O error occurs while opening the file.
+ * @throws SecurityException if a security manager is installed, and it denies read access to the file.
+ */
+ public BufferedChannelImageInputStream(final Path file) throws IOException {
+ this(FileChannel.open(notNull(file, "file"), StandardOpenOption.READ));
+ }
+
+ /**
+ * Constructs a {@code BufferedChannelImageInputStream} that will read from a given {@code RandomAccessFile}.
+ *
+ * Closing this stream will close the {@code RandomAccessFile}.
+ *
+ *
+ * @param file a {@code RandomAccessFile} to read from.
+ * @throws IllegalArgumentException if {@code file} is {@code null}.
+ */
+ public BufferedChannelImageInputStream(final RandomAccessFile file) {
+ // Assumption: Closing the FileChannel will also close its backing RandomAccessFile
+ // (it does in the OpenJDK implementation, and it makes sense, although I can't see this is documented behaviour).
+ this(notNull(file, "file").getChannel());
+ }
+
+ /**
+ * Constructs a {@code BufferedChannelImageInputStream} that will read from a given {@code FileInputStream}.
+ *
+ * Closing this stream will close the {@code FileInputStream}.
+ *
+ *
+ * @param inputStream a {@code FileInputStream} to read from.
+ * @throws IllegalArgumentException if {@code inputStream} is {@code null}.
+ */
+ public BufferedChannelImageInputStream(final FileInputStream inputStream) {
+ // Assumption: Closing the FileChannel will also close its backing FileInputStream (it does in the OpenJDK implementation, although I can't see this is documented).
+ this(notNull(inputStream, "inputStream").getChannel());
+ }
+
+ /**
+ * Constructs a {@code BufferedChannelImageInputStream} that will read from a given {@code SeekableByteChannel}.
+ *
+ * Closing this stream will close the {@code SeekableByteChannel}.
+ *
+ *
+ * @param channel a {@code SeekableByteChannel} to read from.
+ * @throws IllegalArgumentException if {@code channel} is {@code null}.
+ */
+ public BufferedChannelImageInputStream(final SeekableByteChannel channel) {
+ this.channel = notNull(channel, "channel");
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private boolean fillBuffer() throws IOException {
+ byteBuffer.rewind();
+ int length = channel.read(byteBuffer);
+ bufferPos = 0;
+ bufferLimit = max(length, 0);
+
+ return bufferLimit > 0;
+ }
+
+ private boolean bufferEmpty() {
+ return bufferPos >= bufferLimit;
+ }
+
+ @Override
+ public void setByteOrder(ByteOrder byteOrder) {
+ super.setByteOrder(byteOrder);
+ integralCache.order(byteOrder);
+ }
+
+ @Override
+ public int read() throws IOException {
+ checkClosed();
+
+ if (bufferEmpty() && !fillBuffer()) {
+ return -1;
+ }
+
+ bitOffset = 0;
+ streamPos++;
+
+ return buffer[bufferPos++] & 0xff;
+ }
+
+ @Override
+ public int read(final byte[] bytes, final int offset, final int length) throws IOException {
+ checkClosed();
+ bitOffset = 0;
+
+ if (bufferEmpty()) {
+ // Bypass buffer if buffer is empty for reads longer than buffer
+ if (length >= buffer.length) {
+ return readDirect(bytes, offset, length);
+ }
+ else if (!fillBuffer()) {
+ return -1;
+ }
+ }
+
+ int fromBuffer = readBuffered(bytes, offset, length);
+
+ if (length > fromBuffer) {
+ // Due to known bugs in certain JDK-bundled ImageIO plugins expecting read to behave as readFully,
+ // we'll read as much as possible from the buffer, and the rest directly after
+ return fromBuffer + max(0, readDirect(bytes, offset + fromBuffer, length - fromBuffer));
+ }
+
+ return fromBuffer;
+ }
+
+ private int readDirect(final byte[] bytes, final int offset, final int length) throws IOException {
+ // Invalidate the buffer, as its contents is no longer in sync with the stream's position.
+ bufferLimit = 0;
+
+ ByteBuffer wrapped = ByteBuffer.wrap(bytes, offset, length);
+ int read = 0;
+ while (wrapped.hasRemaining()) {
+ int count = channel.read(wrapped);
+ if (count == -1) {
+ if (read == 0) {
+ return -1;
+ }
+
+ break;
+ }
+
+ read += count;
+ }
+
+ streamPos += read;
+
+ return read;
+ }
+
+ private int readBuffered(final byte[] bytes, final int offset, final int length) {
+ // Read as much as possible from buffer
+ int available = Math.min(bufferLimit - bufferPos, length);
+
+ if (available > 0) {
+ System.arraycopy(buffer, bufferPos, bytes, offset, available);
+ bufferPos += available;
+ streamPos += available;
+ }
+
+ return available;
+ }
+
+ public long length() {
+ // WTF?! This method is allowed to throw IOException in the interface...
+ try {
+ checkClosed();
+ return channel.size();
+ }
+ catch (IOException ignore) {
+ }
+
+ return -1;
+ }
+
+ public void close() throws IOException {
+ super.close();
+
+ buffer = null;
+ byteBuffer = null;
+
+ channel.close();
+ channel = null;
+ }
+
+ // Need to override the readShort(), readInt() and readLong() methods,
+ // because the implementations in ImageInputStreamImpl expects the
+ // read(byte[], int, int) to always read the expected number of bytes,
+ // causing uninitialized values, alignment issues and EOFExceptions at
+ // random places...
+ // Notes:
+ // * readUnsignedXx() is covered by their signed counterparts
+ // * readChar() is covered by readShort()
+ // * readFloat() and readDouble() is covered by readInt() and readLong()
+ // respectively.
+ // * readLong() may be covered by two readInt()s, we'll override to be safe
+
+ @Override
+ public short readShort() throws IOException {
+ readFully(integralCacheArray, 0, 2);
+
+ return integralCache.getShort(0);
+ }
+
+ @Override
+ public int readInt() throws IOException {
+ readFully(integralCacheArray, 0, 4);
+
+ return integralCache.getInt(0);
+ }
+
+ @Override
+ public long readLong() throws IOException {
+ readFully(integralCacheArray, 0, 8);
+
+ return integralCache.getLong(0);
+ }
+
+ @Override
+ public void seek(long position) throws IOException {
+ checkClosed();
+
+ if (position < flushedPos) {
+ throw new IndexOutOfBoundsException("position < flushedPos!");
+ }
+
+ bitOffset = 0;
+
+ if (streamPos == position) {
+ return;
+ }
+
+ // Optimized to not invalidate buffer if new position is within current buffer
+ long newBufferPos = bufferPos + position - streamPos;
+ if (newBufferPos >= 0 && newBufferPos < bufferLimit) {
+ bufferPos = (int) newBufferPos;
+ }
+ else {
+ // Will invalidate buffer
+ bufferLimit = 0;
+ channel.position(position);
+ }
+
+ streamPos = position;
+ }
+
+ @Override
+ public void flushBefore(final long pos) throws IOException {
+ super.flushBefore(pos);
+
+ if (channel instanceof MemoryCache) {
+ // In case of memory cache, free up memory
+ ((MemoryCache) channel).flushBefore(pos);
+ }
+ }
+}
diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStream.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStream.java
index e8c98f7e..b53f99f5 100644
--- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStream.java
+++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStream.java
@@ -49,6 +49,7 @@ import static java.lang.Math.max;
* {@link File} or {@link RandomAccessFile} can be used as input.
*
* @see javax.imageio.stream.FileImageInputStream
+ * @deprecated Use {@link BufferedChannelImageInputStream} instead.
*/
// TODO: Create a memory-mapped version?
// Or not... From java.nio.channels.FileChannel.map:
@@ -57,6 +58,7 @@ import static java.lang.Math.max;
// the usual {@link #read read} and {@link #write write} methods. From the
// standpoint of performance it is generally only worth mapping relatively
// large files into memory.
+@Deprecated
public final class BufferedFileImageInputStream extends ImageInputStreamImpl {
static final int DEFAULT_BUFFER_SIZE = 8192;
@@ -190,10 +192,10 @@ public final class BufferedFileImageInputStream extends ImageInputStreamImpl {
public void close() throws IOException {
super.close();
- raf.close();
-
- raf = null;
buffer = null;
+
+ raf.close();
+ raf = null;
}
// Need to override the readShort(), readInt() and readLong() methods,
diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpi.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpi.java
index edc3b169..549c2f99 100644
--- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpi.java
+++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpi.java
@@ -37,18 +37,19 @@ import javax.imageio.spi.ServiceRegistry;
import javax.imageio.stream.ImageInputStream;
import java.io.File;
import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.NoSuchFileException;
import java.util.Iterator;
import java.util.Locale;
/**
* BufferedFileImageInputStreamSpi
- * Experimental
*
* @author Harald Kuhr
* @author last modified by $Author: haraldk$
* @version $Id: BufferedFileImageInputStreamSpi.java,v 1.0 May 15, 2008 2:14:59 PM haraldk Exp$
*/
-public class BufferedFileImageInputStreamSpi extends ImageInputStreamSpi {
+public final class BufferedFileImageInputStreamSpi extends ImageInputStreamSpi {
public BufferedFileImageInputStreamSpi() {
this(new StreamProviderInfo());
}
@@ -69,12 +70,13 @@ public class BufferedFileImageInputStreamSpi extends ImageInputStreamSpi {
}
}
- public ImageInputStream createInputStreamInstance(final Object input, final boolean pUseCache, final File pCacheDir) {
+ @Override
+ public ImageInputStream createInputStreamInstance(final Object input, final boolean useCacheFile, final File cacheDir) throws IOException {
if (input instanceof File) {
try {
- return new BufferedFileImageInputStream((File) input);
+ return new BufferedChannelImageInputStream((File) input);
}
- catch (FileNotFoundException e) {
+ catch (FileNotFoundException | NoSuchFileException e) {
// For consistency with the JRE bundled SPIs, we'll return null here,
// even though the spec does not say that's allowed.
// The problem is that the SPIs can only declare that they support an input type like a File,
@@ -91,7 +93,8 @@ public class BufferedFileImageInputStreamSpi extends ImageInputStreamSpi {
return false;
}
- public String getDescription(final Locale pLocale) {
+ @Override
+ public String getDescription(final Locale locale) {
return "Service provider that instantiates an ImageInputStream from a File";
}
diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedInputStreamImageInputStreamSpi.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedInputStreamImageInputStreamSpi.java
new file mode 100644
index 00000000..cf22875f
--- /dev/null
+++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedInputStreamImageInputStreamSpi.java
@@ -0,0 +1,78 @@
+package com.twelvemonkeys.imageio.stream;
+
+import com.twelvemonkeys.imageio.spi.ProviderInfo;
+
+import javax.imageio.spi.ImageInputStreamSpi;
+import javax.imageio.spi.ServiceRegistry;
+import javax.imageio.stream.ImageInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.util.Iterator;
+import java.util.Locale;
+
+/**
+ * BufferedInputStreamImageInputStreamSpi.
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: BufferedInputStreamImageInputStreamSpi.java,v 1.0 08/09/2022 haraldk Exp$
+ */
+public final class BufferedInputStreamImageInputStreamSpi extends ImageInputStreamSpi {
+ public BufferedInputStreamImageInputStreamSpi() {
+ this(new StreamProviderInfo());
+ }
+
+ private BufferedInputStreamImageInputStreamSpi(ProviderInfo providerInfo) {
+ super(providerInfo.getVendorName(), providerInfo.getVersion(), InputStream.class);
+ }
+
+ @Override
+ public void onRegistration(final ServiceRegistry registry, final Class> category) {
+ Iterator providers = registry.getServiceProviders(ImageInputStreamSpi.class, new InputStreamFilter(), true);
+
+ while (providers.hasNext()) {
+ ImageInputStreamSpi provider = providers.next();
+ if (provider != this) {
+ registry.setOrdering(ImageInputStreamSpi.class, this, provider);
+ }
+ }
+ }
+
+ @Override
+ public ImageInputStream createInputStreamInstance(final Object input, final boolean useCacheFile, final File cacheDir) throws IOException {
+ if (input instanceof InputStream) {
+ ReadableByteChannel channel = Channels.newChannel((InputStream) input);
+
+ if (channel instanceof SeekableByteChannel) {
+ // Special case for FileInputStream/FileChannel, we can get a seekable channel directly
+ return new BufferedChannelImageInputStream((SeekableByteChannel) channel);
+ }
+
+ // Otherwise, create a cache for backwards seeking
+ return new BufferedChannelImageInputStream(useCacheFile ? new DiskCache(channel, cacheDir): new MemoryCache(channel));
+ }
+
+ throw new IllegalArgumentException("Expected input of type InputStream: " + input);
+ }
+
+ @Override
+ public boolean canUseCacheFile() {
+ return true;
+ }
+
+ @Override
+ public String getDescription(final Locale locale) {
+ return "Service provider that instantiates an ImageInputStream from an InputStream";
+ }
+
+ private static class InputStreamFilter implements ServiceRegistry.Filter {
+ @Override
+ public boolean filter(final Object provider) {
+ return ((ImageInputStreamSpi) provider).getInputClass() == InputStream.class;
+ }
+ }
+}
diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedRAFImageInputStreamSpi.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedRAFImageInputStreamSpi.java
index 69bac835..7b679b0f 100644
--- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedRAFImageInputStreamSpi.java
+++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedRAFImageInputStreamSpi.java
@@ -48,7 +48,7 @@ import java.util.Locale;
* @author last modified by $Author: haraldk$
* @version $Id: BufferedRAFImageInputStreamSpi.java,v 1.0 May 15, 2008 2:14:59 PM haraldk Exp$
*/
-public class BufferedRAFImageInputStreamSpi extends ImageInputStreamSpi {
+public final class BufferedRAFImageInputStreamSpi extends ImageInputStreamSpi {
public BufferedRAFImageInputStreamSpi() {
this(new StreamProviderInfo());
}
@@ -69,9 +69,10 @@ public class BufferedRAFImageInputStreamSpi extends ImageInputStreamSpi {
}
}
- public ImageInputStream createInputStreamInstance(final Object input, final boolean pUseCache, final File pCacheDir) {
+ @Override
+ public ImageInputStream createInputStreamInstance(final Object input, final boolean useCacheFile, final File cacheDir) {
if (input instanceof RandomAccessFile) {
- return new BufferedFileImageInputStream((RandomAccessFile) input);
+ return new BufferedChannelImageInputStream((RandomAccessFile) input);
}
throw new IllegalArgumentException("Expected input of type RandomAccessFile: " + input);
@@ -82,7 +83,8 @@ public class BufferedRAFImageInputStreamSpi extends ImageInputStreamSpi {
return false;
}
- public String getDescription(final Locale pLocale) {
+ @Override
+ public String getDescription(final Locale locale) {
return "Service provider that instantiates an ImageInputStream from a RandomAccessFile";
}
diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStreamSpi.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStreamSpi.java
index 29655d1c..dac0d4da 100755
--- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStreamSpi.java
+++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStreamSpi.java
@@ -45,7 +45,7 @@ import java.util.Locale;
* @author last modified by $Author: haraldk$
* @version $Id: ByteArrayImageInputStreamSpi.java,v 1.0 May 15, 2008 2:12:12 PM haraldk Exp$
*/
-public class ByteArrayImageInputStreamSpi extends ImageInputStreamSpi {
+public final class ByteArrayImageInputStreamSpi extends ImageInputStreamSpi {
public ByteArrayImageInputStreamSpi() {
this(new StreamProviderInfo());
@@ -55,16 +55,17 @@ public class ByteArrayImageInputStreamSpi extends ImageInputStreamSpi {
super(providerInfo.getVendorName(), providerInfo.getVersion(), byte[].class);
}
- public ImageInputStream createInputStreamInstance(Object pInput, boolean pUseCache, File pCacheDir) {
- if (pInput instanceof byte[]) {
- return new ByteArrayImageInputStream((byte[]) pInput);
- }
- else {
- throw new IllegalArgumentException("Expected input of type byte[]: " + pInput);
+ @Override
+ public ImageInputStream createInputStreamInstance(Object input, boolean useCacheFile, File cacheDir) {
+ if (input instanceof byte[]) {
+ return new ByteArrayImageInputStream((byte[]) input);
}
+
+ throw new IllegalArgumentException("Expected input of type byte[]: " + input);
}
- public String getDescription(Locale pLocale) {
+ @Override
+ public String getDescription(Locale locale) {
return "Service provider that instantiates an ImageInputStream from a byte array";
}
diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/DirectImageInputStream.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/DirectImageInputStream.java
index bff04e3b..35a9af44 100644
--- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/DirectImageInputStream.java
+++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/DirectImageInputStream.java
@@ -125,7 +125,7 @@ public final class DirectImageInputStream extends ImageInputStreamImpl {
@Override
public void close() throws IOException {
- // We could seek to EOF here, but the usual case
+ // We could seek to EOF here, but the usual case is we know where the next chunk of data is
stream.close();
super.close();
diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/DiskCache.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/DiskCache.java
new file mode 100644
index 00000000..18af816b
--- /dev/null
+++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/DiskCache.java
@@ -0,0 +1,114 @@
+package com.twelvemonkeys.imageio.stream;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.nio.channels.NonWritableChannelException;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static com.twelvemonkeys.lang.Validate.isTrue;
+import static com.twelvemonkeys.lang.Validate.notNull;
+import static java.lang.Math.max;
+import static java.nio.file.StandardOpenOption.DELETE_ON_CLOSE;
+import static java.nio.file.StandardOpenOption.READ;
+import static java.nio.file.StandardOpenOption.WRITE;
+
+// Note: We could consider creating a memory-mapped version...
+// But, from java.nio.channels.FileChannel.map:
+// For most operating systems, mapping a file into memory is more
+// expensive than reading or writing a few tens of kilobytes of data via
+// the usual {@link #read read} and {@link #write write} methods. From the
+// standpoint of performance it is generally only worth mapping relatively
+// large files into memory.
+final class DiskCache implements SeekableByteChannel {
+ final static int BLOCK_SIZE = 1 << 13;
+
+ private final FileChannel cache;
+ private final ReadableByteChannel channel;
+
+ // TODO: Perhaps skip this constructor?
+ DiskCache(InputStream stream, File cacheDir) throws IOException {
+ // Stream will be closed with channel, documented behavior
+ this(Channels.newChannel(notNull(stream, "stream")), cacheDir);
+ }
+
+ public DiskCache(ReadableByteChannel channel, File cacheDir) throws IOException {
+ this.channel = notNull(channel, "channel");
+ isTrue(cacheDir == null || cacheDir.isDirectory(), cacheDir, "%s is not a directory");
+
+ // Create a temp file to hold our cache,
+ // will be deleted when this channel is closed, as we close the cache
+ Path cacheFile = cacheDir == null
+ ? Files.createTempFile("imageio", ".tmp")
+ : Files.createTempFile(cacheDir.toPath(), "imageio", ".tmp");
+
+ cache = FileChannel.open(cacheFile, DELETE_ON_CLOSE, READ, WRITE);
+ }
+
+ @SuppressWarnings("StatementWithEmptyBody")
+ void fetch() throws IOException {
+ while (cache.position() >= cache.size() && cache.transferFrom(channel, cache.size(), max(cache.position() - cache.size(), BLOCK_SIZE)) > 0) {
+ // Continue transfer...
+ }
+ }
+
+ @Override
+ public boolean isOpen() {
+ return channel.isOpen();
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ cache.close();
+ }
+ finally {
+ channel.close();
+ }
+ }
+
+ @Override
+ public int read(ByteBuffer dest) throws IOException {
+ fetch();
+
+ if (cache.position() >= cache.size()) {
+ return -1;
+ }
+
+ return cache.read(dest);
+ }
+
+ @Override
+ public long position() throws IOException {
+ return cache.position();
+ }
+
+ @Override
+ public SeekableByteChannel position(long newPosition) throws IOException {
+ cache.position(newPosition);
+ return this;
+ }
+
+ @Override
+ public long size() {
+ // We could allow the size to grow, but that means the stream cannot rely on this size, so we'll just pretend we don't know...
+ return -1;
+ }
+
+ @Override
+ public int write(ByteBuffer src) {
+ throw new NonWritableChannelException();
+ }
+
+ @Override
+ public SeekableByteChannel truncate(long size) {
+ throw new NonWritableChannelException();
+ }
+}
+
diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/MemoryCache.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/MemoryCache.java
new file mode 100644
index 00000000..e0f86bee
--- /dev/null
+++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/MemoryCache.java
@@ -0,0 +1,156 @@
+package com.twelvemonkeys.imageio.stream;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.NonWritableChannelException;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.twelvemonkeys.lang.Validate.notNull;
+import static java.lang.Math.min;
+
+public final class MemoryCache implements SeekableByteChannel {
+
+ final static int BLOCK_SIZE = 1 << 13;
+
+ private final List cache = new ArrayList<>();
+ private final ReadableByteChannel channel;
+ private long length;
+ private long position;
+ private long start;
+
+ // TODO: Maybe get rid of this constructor, as we don't want to do this if we have a FileInputStream/FileChannel...
+ MemoryCache(InputStream stream) {
+ this(Channels.newChannel(notNull(stream, "stream")));
+ }
+
+ public MemoryCache(ReadableByteChannel channel) {
+ this.channel = notNull(channel, "channel");
+ }
+
+ byte[] fetchBlock() throws IOException {
+ long currPos = position;
+
+ long index = currPos / BLOCK_SIZE;
+
+ if (index >= Integer.MAX_VALUE) {
+ throw new IOException("Memory cache max size exceeded");
+ }
+
+ while (index >= cache.size()) {
+ byte[] block;
+ try {
+ block = new byte[BLOCK_SIZE];
+ }
+ catch (OutOfMemoryError e) {
+ throw new IOException("No more memory for cache: " + cache.size() * BLOCK_SIZE);
+ }
+
+ cache.add(block);
+ length += readBlock(block);
+ }
+
+ return cache.get((int) index);
+ }
+
+ private int readBlock(final byte[] block) throws IOException {
+ ByteBuffer wrapped = ByteBuffer.wrap(block);
+
+ while (wrapped.hasRemaining()) {
+ int count = channel.read(wrapped);
+ if (count == -1) {
+ // Last block
+ break;
+ }
+ }
+
+ return wrapped.position();
+ }
+
+ @Override
+ public boolean isOpen() {
+ return channel.isOpen();
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ cache.clear();
+ }
+ finally {
+ channel.close();
+ }
+ }
+
+ @Override
+ public int read(ByteBuffer dest) throws IOException {
+ byte[] buffer = fetchBlock();
+ int bufferPos = (int) (position % BLOCK_SIZE);
+
+ if (position >= length) {
+ return -1;
+ }
+
+ int len = min(dest.remaining(), (int) min(BLOCK_SIZE - bufferPos, length - position));
+ dest.put(buffer, bufferPos, len);
+
+ position += len;
+
+ return len;
+ }
+
+ @Override
+ public long position() throws IOException {
+ return position;
+ }
+
+ @Override
+ public SeekableByteChannel position(long newPosition) throws IOException {
+ if (newPosition < start) {
+ throw new IOException("Seek before flush position");
+ }
+
+ this.position = newPosition;
+
+ return this;
+ }
+
+ @Override
+ public long size() throws IOException {
+ // We could allow the size to grow, but that means the stream cannot rely on this size, so we'll just pretend we don't know...
+ return -1;
+ }
+
+ @Override
+ public int write(ByteBuffer src) {
+ throw new NonWritableChannelException();
+ }
+
+ @Override
+ public SeekableByteChannel truncate(long size) {
+ throw new NonWritableChannelException();
+ }
+
+ void flushBefore(long pos) {
+ if (pos < start) {
+ throw new IndexOutOfBoundsException("pos < flushed position");
+ }
+ if (pos > position) {
+ throw new IndexOutOfBoundsException("pos > current position");
+ }
+
+ int blocks = (int) (pos / BLOCK_SIZE); // Overflow guarded for in fetchBlock
+
+ // Clear blocks no longer needed
+ for (int i = 0; i < blocks; i++) {
+ cache.set(i, null);
+ }
+
+ start = pos;
+ }
+}
+
diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/URLImageInputStreamSpi.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/URLImageInputStreamSpi.java
index b56204c7..f59b9c30 100644
--- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/URLImageInputStreamSpi.java
+++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/URLImageInputStreamSpi.java
@@ -33,9 +33,7 @@ package com.twelvemonkeys.imageio.stream;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
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;
@@ -52,7 +50,7 @@ import java.util.Locale;
* @version $Id: URLImageInputStreamSpi.java,v 1.0 May 15, 2008 2:14:59 PM haraldk Exp$
*/
// TODO: URI instead of URL?
-public class URLImageInputStreamSpi extends ImageInputStreamSpi {
+public final class URLImageInputStreamSpi extends ImageInputStreamSpi {
public URLImageInputStreamSpi() {
this(new StreamProviderInfo());
}
@@ -64,53 +62,28 @@ public class URLImageInputStreamSpi extends ImageInputStreamSpi {
// TODO: Create a URI or URLImageInputStream class, with a getUR[I|L] method, to allow for multiple file formats
// The good thing with that is that it does not clash with the built-in Sun-stuff or other people's hacks
// The bad thing is that most people don't expect there to be an UR[I|L]ImageInputStreamSpi..
- public ImageInputStream createInputStreamInstance(final Object pInput, final boolean pUseCache, final File pCacheDir) throws IOException {
- if (pInput instanceof URL) {
- URL url = (URL) pInput;
+ @Override
+ public ImageInputStream createInputStreamInstance(final Object input, final boolean useCacheFile, final File cacheDir) throws IOException {
+ if (input instanceof URL) {
+ URL url = (URL) input;
// Special case for file protocol, a lot faster than FileCacheImageInputStream
if ("file".equals(url.getProtocol())) {
try {
- return new BufferedFileImageInputStream(new File(url.toURI()));
+ return new BufferedChannelImageInputStream(new File(url.toURI()));
}
- catch (URISyntaxException ignore) {
- // This should never happen, but if it does, we'll fall back to using the stream
- ignore.printStackTrace();
+ catch (URISyntaxException shouldNeverHappen) {
+ // This should never happen, but if it does, we'll fall back to using the stream
+ shouldNeverHappen.printStackTrace();
}
}
// Otherwise revert to cached
- final InputStream urlStream = url.openStream();
- if (pUseCache) {
- return new FileCacheImageInputStream(urlStream, pCacheDir) {
- @Override
- public void close() throws IOException {
- try {
- super.close();
- }
- finally {
- urlStream.close(); // NOTE: If this line throws IOE, it will shadow the original..
- }
- }
- };
- }
- else {
- return new MemoryCacheImageInputStream(urlStream) {
- @Override
- public void close() throws IOException {
- try {
- super.close();
- }
- finally {
- urlStream.close(); // NOTE: If this line throws IOE, it will shadow the original..
- }
- }
- };
- }
- }
- else {
- throw new IllegalArgumentException("Expected input of type URL: " + pInput);
+ InputStream urlStream = url.openStream();
+ return new BufferedChannelImageInputStream(useCacheFile ? new DiskCache(urlStream, cacheDir) : new MemoryCache(urlStream));
}
+
+ throw new IllegalArgumentException("Expected input of type URL: " + input);
}
@Override
@@ -118,7 +91,7 @@ public class URLImageInputStreamSpi extends ImageInputStreamSpi {
return true;
}
- public String getDescription(final Locale pLocale) {
+ public String getDescription(final Locale locale) {
return "Service provider that instantiates an ImageInputStream from a URL";
}
}
diff --git a/imageio/imageio-core/src/main/resources/META-INF/services/javax.imageio.spi.ImageInputStreamSpi b/imageio/imageio-core/src/main/resources/META-INF/services/javax.imageio.spi.ImageInputStreamSpi
index d00c2f2d..b6d0079c 100644
--- a/imageio/imageio-core/src/main/resources/META-INF/services/javax.imageio.spi.ImageInputStreamSpi
+++ b/imageio/imageio-core/src/main/resources/META-INF/services/javax.imageio.spi.ImageInputStreamSpi
@@ -1,4 +1,5 @@
com.twelvemonkeys.imageio.stream.BufferedFileImageInputStreamSpi
com.twelvemonkeys.imageio.stream.BufferedRAFImageInputStreamSpi
+com.twelvemonkeys.imageio.stream.BufferedInputStreamImageInputStreamSpi
# Use SPI loading as a hook for early profile activation
com.twelvemonkeys.imageio.color.ProfileDeferralActivator$Spi
diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedChannelImageInputStreamDiskCacheTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedChannelImageInputStreamDiskCacheTest.java
new file mode 100755
index 00000000..8050476e
--- /dev/null
+++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedChannelImageInputStreamDiskCacheTest.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright (c) 2020, 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 of the copyright holder 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 HOLDER 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.imageio.stream;
+
+import org.junit.Test;
+import org.junit.function.ThrowingRunnable;
+
+import javax.imageio.stream.ImageInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.ReadableByteChannel;
+import java.util.Random;
+
+import static com.twelvemonkeys.imageio.stream.BufferedImageInputStreamTest.rangeEquals;
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.verify;
+
+/**
+ * BufferedFileImageInputStreamTestCase
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: BufferedFileImageInputStreamTestCase.java,v 1.0 Apr 21, 2009 10:58:48 AM haraldk Exp$
+ */
+// TODO: Remove this test, and instead test the disk cache directly!
+public class BufferedChannelImageInputStreamDiskCacheTest {
+ private final Random random = new Random(170984354357234566L);
+
+ private InputStream randomDataToInputStream(byte[] data) {
+ random.nextBytes(data);
+
+ return new ByteArrayInputStream(data);
+ }
+
+ @Test
+ public void testCreate() throws IOException {
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new DiskCache(new ByteArrayInputStream(new byte[0]), null))) {
+ assertEquals("Stream length should be unknown", -1, stream.length());
+ }
+ }
+
+ @Test
+ public void testCreateNullStream() throws IOException {
+ try {
+ new DiskCache((InputStream) null, null);
+ fail("Expected IllegalArgumentException");
+ }
+ catch (IllegalArgumentException expected) {
+ assertNotNull("Null exception message", expected.getMessage());
+ String message = expected.getMessage().toLowerCase();
+ assertTrue("Exception message does not contain parameter name", message.contains("stream"));
+ assertTrue("Exception message does not contain null", message.contains("null"));
+ }
+ }
+
+ @Test
+ public void testCreateNullChannel() throws IOException {
+ try {
+ new DiskCache((ReadableByteChannel) null, null);
+ fail("Expected IllegalArgumentException");
+ }
+ catch (IllegalArgumentException expected) {
+ assertNotNull("Null exception message", expected.getMessage());
+ String message = expected.getMessage().toLowerCase();
+ assertTrue("Exception message does not contain parameter name", message.contains("channel"));
+ assertTrue("Exception message does not contain null", message.contains("null"));
+ }
+ }
+
+ @Test
+ public void testRead() throws IOException {
+ byte[] data = new byte[1024 * 1024];
+ InputStream input = randomDataToInputStream(data);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new DiskCache(input, null))) {
+ assertEquals("Stream length should be unknown", -1, stream.length());
+
+ for (byte value : data) {
+ assertEquals("Wrong data read", value & 0xff, stream.read());
+ }
+
+ assertEquals("Wrong data read", -1, stream.read());
+ }
+ }
+
+ @Test
+ public void testReadArray() throws IOException {
+ byte[] data = new byte[1024 * 1024];
+ InputStream input = randomDataToInputStream(data);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new DiskCache(input, null))) {
+ assertEquals("Stream length should be unknown", -1, stream.length());
+
+ byte[] result = new byte[1024];
+
+ for (int i = 0; i < data.length / result.length; i++) {
+ stream.readFully(result);
+ assertTrue("Wrong data read: " + i, rangeEquals(data, i * result.length, result, 0, result.length));
+ }
+
+ assertEquals("Wrong data read", -1, stream.read());
+ }
+ }
+
+ @Test
+ public void testReadSkip() throws IOException {
+ byte[] data = new byte[1024 * 14];
+ InputStream input = randomDataToInputStream(data);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new DiskCache(input, null))) {
+ assertEquals("Stream length should be unknown", -1, stream.length());
+
+ byte[] result = new byte[7];
+
+ for (int i = 0; i < data.length / result.length; i += 2) {
+ stream.readFully(result);
+ stream.skipBytes(result.length);
+ assertTrue("Wrong data read: " + i, rangeEquals(data, i * result.length, result, 0, result.length));
+ }
+ }
+ }
+
+ @Test
+ public void testReadSeek() throws IOException {
+ byte[] data = new byte[1024 * 18];
+ InputStream input = randomDataToInputStream(data);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new DiskCache(input, null))) {
+ assertEquals("Stream length should be unknown", -1, stream.length());
+
+ byte[] result = new byte[9];
+
+ for (int i = 0; i < data.length / result.length; i++) {
+ // Read backwards
+ long newPos = data.length - result.length - i * result.length;
+ stream.seek(newPos);
+ assertEquals("Wrong stream position", newPos, stream.getStreamPosition());
+ stream.readFully(result);
+ assertTrue("Wrong data read: " + i, rangeEquals(data, (int) newPos, result, 0, result.length));
+ }
+ }
+ }
+
+ @Test
+ public void testReadOutsideDataSeek0Read() throws IOException {
+ byte[] data = new byte[256];
+ InputStream input = randomDataToInputStream(data);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new DiskCache(input, null))) {
+ assertEquals("Stream length should be unknown", -1, stream.length());
+
+ byte[] buffer = new byte[data.length * 2];
+ stream.read(buffer);
+ stream.seek(0);
+ assertNotEquals(-1, stream.read());
+ assertNotEquals(-1, stream.read(buffer));
+ }
+ }
+
+ @Test
+ public void testReadBitRandom() throws IOException {
+ byte[] bytes = new byte[8];
+ InputStream input = randomDataToInputStream(bytes);
+ long value = ByteBuffer.wrap(bytes).getLong();
+
+ // Create stream
+ try (ImageInputStream stream = new BufferedChannelImageInputStream(new DiskCache(input, null))) {
+ for (int i = 1; i <= 64; i++) {
+ assertEquals(String.format("bit %d differ", i), (value << (i - 1L)) >>> 63L, stream.readBit());
+ }
+ }
+ }
+
+ @Test
+ public void testReadBitsRandom() throws IOException {
+ byte[] bytes = new byte[8];
+ InputStream input = randomDataToInputStream(bytes);
+ long value = ByteBuffer.wrap(bytes).getLong();
+
+ // Create stream
+ try (ImageInputStream stream = new BufferedChannelImageInputStream(new DiskCache(input, null))) {
+ for (int i = 1; i <= 64; i++) {
+ stream.seek(0);
+ assertEquals(String.format("bit %d differ", i), value >>> (64L - i), stream.readBits(i));
+ assertEquals(i % 8, stream.getBitOffset());
+ }
+ }
+ }
+
+ @Test
+ public void testReadBitsRandomOffset() throws IOException {
+ byte[] bytes = new byte[8];
+ InputStream input = randomDataToInputStream(bytes);
+ long value = ByteBuffer.wrap(bytes).getLong();
+
+ // Create stream
+ try (ImageInputStream stream = new BufferedChannelImageInputStream(new DiskCache(input, null))) {
+ for (int i = 1; i <= 60; i++) {
+ stream.seek(0);
+ stream.setBitOffset(i % 8);
+ assertEquals(String.format("bit %d differ", i), (value << (i % 8)) >>> (64L - i), stream.readBits(i));
+ assertEquals(i * 2 % 8, stream.getBitOffset());
+ }
+ }
+ }
+
+ @Test
+ public void testReadShort() throws IOException {
+ byte[] bytes = new byte[8743]; // Slightly more than one buffer size
+ InputStream input = randomDataToInputStream(bytes);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
+
+ try (final ImageInputStream stream = new BufferedChannelImageInputStream(new DiskCache(input, null))) {
+ stream.setByteOrder(ByteOrder.BIG_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 2; i++) {
+ assertEquals(buffer.getShort(), stream.readShort());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readShort();
+ }
+ });
+
+ stream.seek(0);
+ stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ buffer.position(0);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 2; i++) {
+ assertEquals(buffer.getShort(), stream.readShort());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readShort();
+ }
+ });
+ }
+ }
+
+ @Test
+ public void testReadInt() throws IOException {
+ byte[] bytes = new byte[8743]; // Slightly more than one buffer size
+ InputStream input = randomDataToInputStream(bytes);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
+
+ try (final ImageInputStream stream = new BufferedChannelImageInputStream(new DiskCache(input, null))) {
+ stream.setByteOrder(ByteOrder.BIG_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 4; i++) {
+ assertEquals(buffer.getInt(), stream.readInt());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readInt();
+ }
+ });
+
+ stream.seek(0);
+ stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ buffer.position(0);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 4; i++) {
+ assertEquals(buffer.getInt(), stream.readInt());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readInt();
+ }
+ });
+ }
+ }
+
+ @Test
+ public void testReadLong() throws IOException {
+ byte[] bytes = new byte[8743]; // Slightly more than one buffer size
+ InputStream input = randomDataToInputStream(bytes);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
+
+ try (final ImageInputStream stream = new BufferedChannelImageInputStream(new DiskCache(input, null))) {
+ stream.setByteOrder(ByteOrder.BIG_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 8; i++) {
+ assertEquals(buffer.getLong(), stream.readLong());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readLong();
+ }
+ });
+
+ stream.seek(0);
+ stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ buffer.position(0);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 8; i++) {
+ assertEquals(buffer.getLong(), stream.readLong());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readLong();
+ }
+ });
+ }
+ }
+
+ @Test
+ public void testSeekPastEOF() throws IOException {
+ byte[] bytes = new byte[9];
+ InputStream input = randomDataToInputStream(bytes);
+
+ try (final ImageInputStream stream = new BufferedChannelImageInputStream(new DiskCache(input, null))) {
+ stream.seek(1000);
+
+ assertEquals(-1, stream.read());
+ assertEquals(-1, stream.read(new byte[1], 0, 1));
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readFully(new byte[1]);
+ }
+ });
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readByte();
+ }
+ });
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readShort();
+ }
+ });
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readInt();
+ }
+ });
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readLong();
+ }
+ });
+
+ stream.seek(0);
+ for (byte value : bytes) {
+ assertEquals(value, stream.readByte());
+ }
+
+ assertEquals(-1, stream.read());
+ }
+ }
+
+ @Test
+ public void testClose() throws IOException {
+ // Create wrapper stream
+ InputStream mock = mock(InputStream.class);
+ ImageInputStream stream = new BufferedChannelImageInputStream(new DiskCache(mock, null));
+
+ stream.close();
+ verify(mock, only()).close();
+ }
+
+ @Test
+ public void testWorkaroundForWBMPImageReaderExpectsReadToBehaveAsReadFully() throws IOException {
+ // See #606 for details.
+ // Bug in JDK WBMPImageReader, uses read(byte[], int, int) instead of readFully(byte[], int, int).
+ // Ie: Relies on read to return all bytes at once, without blocking
+ int size = BufferedChannelImageInputStream.DEFAULT_BUFFER_SIZE * 7;
+ byte[] bytes = new byte[size];
+ InputStream input = randomDataToInputStream(bytes);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new DiskCache(input, null))) {
+ byte[] result = new byte[size];
+ int head = stream.read(result, 0, 12); // Provoke a buffered read
+ int len = stream.read(result, 12, size - 12); // Rest of buffer + direct read
+
+ assertEquals(size, len + head);
+ assertArrayEquals(bytes, result);
+ }
+ }
+}
diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedChannelImageInputStreamMemoryCacheTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedChannelImageInputStreamMemoryCacheTest.java
new file mode 100755
index 00000000..00206a71
--- /dev/null
+++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedChannelImageInputStreamMemoryCacheTest.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright (c) 2020, 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 of the copyright holder 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 HOLDER 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.imageio.stream;
+
+import org.junit.Test;
+import org.junit.function.ThrowingRunnable;
+
+import javax.imageio.stream.ImageInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.ReadableByteChannel;
+import java.util.Random;
+
+import static com.twelvemonkeys.imageio.stream.BufferedImageInputStreamTest.rangeEquals;
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.verify;
+
+/**
+ * BufferedFileImageInputStreamTestCase
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: BufferedFileImageInputStreamTestCase.java,v 1.0 Apr 21, 2009 10:58:48 AM haraldk Exp$
+ */
+// TODO: Remove this test, and instead test the memory cache directly!
+public class BufferedChannelImageInputStreamMemoryCacheTest {
+ private final Random random = new Random(170984354357234566L);
+
+ private InputStream randomDataToInputStream(byte[] data) {
+ random.nextBytes(data);
+
+ return new ByteArrayInputStream(data);
+ }
+
+ @Test
+ public void testCreate() throws IOException {
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(new ByteArrayInputStream(new byte[0])))) {
+ assertEquals("Stream length should be unknown", -1, stream.length());
+ }
+ }
+
+ @Test
+ public void testCreateNullStream() {
+ try {
+ new MemoryCache((InputStream) null);
+ fail("Expected IllegalArgumentException");
+ }
+ catch (IllegalArgumentException expected) {
+ assertNotNull("Null exception message", expected.getMessage());
+ String message = expected.getMessage().toLowerCase();
+ assertTrue("Exception message does not contain parameter name", message.contains("stream"));
+ assertTrue("Exception message does not contain null", message.contains("null"));
+ }
+ }
+
+ @Test
+ public void testCreateNullChannel() {
+ try {
+ new MemoryCache((ReadableByteChannel) null);
+ fail("Expected IllegalArgumentException");
+ }
+ catch (IllegalArgumentException expected) {
+ assertNotNull("Null exception message", expected.getMessage());
+ String message = expected.getMessage().toLowerCase();
+ assertTrue("Exception message does not contain parameter name", message.contains("channel"));
+ assertTrue("Exception message does not contain null", message.contains("null"));
+ }
+ }
+
+ @Test
+ public void testRead() throws IOException {
+ byte[] data = new byte[1024 * 1024];
+ InputStream input = randomDataToInputStream(data);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
+ assertEquals("Stream length should be unknown", -1, stream.length());
+
+ for (byte value : data) {
+ assertEquals("Wrong data read", value & 0xff, stream.read());
+ }
+
+ assertEquals("Wrong data read", -1, stream.read());
+ }
+ }
+
+ @Test
+ public void testReadArray() throws IOException {
+ byte[] data = new byte[1024 * 1024];
+ InputStream input = randomDataToInputStream(data);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
+ assertEquals("Stream length should be unknown", -1, stream.length());
+
+ byte[] result = new byte[1024];
+
+ for (int i = 0; i < data.length / result.length; i++) {
+ stream.readFully(result);
+ assertTrue("Wrong data read: " + i, rangeEquals(data, i * result.length, result, 0, result.length));
+ }
+
+ assertEquals("Wrong data read", -1, stream.read());
+ }
+ }
+
+ @Test
+ public void testReadSkip() throws IOException {
+ byte[] data = new byte[1024 * 14];
+ InputStream input = randomDataToInputStream(data);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
+ assertEquals("Stream length should be unknown", -1, stream.length());
+
+ byte[] result = new byte[7];
+
+ for (int i = 0; i < data.length / result.length; i += 2) {
+ stream.readFully(result);
+ stream.skipBytes(result.length);
+ assertTrue("Wrong data read: " + i, rangeEquals(data, i * result.length, result, 0, result.length));
+ }
+ }
+ }
+
+ @Test
+ public void testReadSeek() throws IOException {
+ byte[] data = new byte[1024 * 18];
+ InputStream input = randomDataToInputStream(data);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
+ assertEquals("Stream length should be unknown", -1, stream.length());
+
+ byte[] result = new byte[9];
+
+ for (int i = 0; i < data.length / result.length; i++) {
+ // Read backwards
+ long newPos = data.length - result.length - i * result.length;
+ stream.seek(newPos);
+ assertEquals("Wrong stream position", newPos, stream.getStreamPosition());
+ stream.readFully(result);
+ assertTrue("Wrong data read: " + i, rangeEquals(data, (int) newPos, result, 0, result.length));
+ }
+ }
+ }
+
+ @Test
+ public void testReadOutsideDataSeek0Read() throws IOException {
+ byte[] data = new byte[256];
+ InputStream input = randomDataToInputStream(data);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
+ assertEquals("Stream length should be unknown", -1, stream.length());
+
+ byte[] buffer = new byte[data.length * 2];
+ stream.read(buffer);
+ stream.seek(0);
+ assertNotEquals(-1, stream.read());
+ assertNotEquals(-1, stream.read(buffer));
+ }
+ }
+
+ @Test
+ public void testReadBitRandom() throws IOException {
+ byte[] bytes = new byte[8];
+ InputStream input = randomDataToInputStream(bytes);
+ long value = ByteBuffer.wrap(bytes).getLong();
+
+ // Create stream
+ try (ImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
+ for (int i = 1; i <= 64; i++) {
+ assertEquals(String.format("bit %d differ", i), (value << (i - 1L)) >>> 63L, stream.readBit());
+ }
+ }
+ }
+
+ @Test
+ public void testReadBitsRandom() throws IOException {
+ byte[] bytes = new byte[8];
+ InputStream input = randomDataToInputStream(bytes);
+ long value = ByteBuffer.wrap(bytes).getLong();
+
+ // Create stream
+ try (ImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
+ for (int i = 1; i <= 64; i++) {
+ stream.seek(0);
+ assertEquals(String.format("bit %d differ", i), value >>> (64L - i), stream.readBits(i));
+ assertEquals(i % 8, stream.getBitOffset());
+ }
+ }
+ }
+
+ @Test
+ public void testReadBitsRandomOffset() throws IOException {
+ byte[] bytes = new byte[8];
+ InputStream input = randomDataToInputStream(bytes);
+ long value = ByteBuffer.wrap(bytes).getLong();
+
+ // Create stream
+ try (ImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
+ for (int i = 1; i <= 60; i++) {
+ stream.seek(0);
+ stream.setBitOffset(i % 8);
+ assertEquals(String.format("bit %d differ", i), (value << (i % 8)) >>> (64L - i), stream.readBits(i));
+ assertEquals(i * 2 % 8, stream.getBitOffset());
+ }
+ }
+ }
+
+ @Test
+ public void testReadShort() throws IOException {
+ byte[] bytes = new byte[8743]; // Slightly more than one buffer size
+ InputStream input = randomDataToInputStream(bytes);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
+
+ try (final ImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
+ stream.setByteOrder(ByteOrder.BIG_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 2; i++) {
+ assertEquals(buffer.getShort(), stream.readShort());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readShort();
+ }
+ });
+
+ stream.seek(0);
+ stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ buffer.position(0);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 2; i++) {
+ assertEquals(buffer.getShort(), stream.readShort());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readShort();
+ }
+ });
+ }
+ }
+
+ @Test
+ public void testReadInt() throws IOException {
+ byte[] bytes = new byte[8743]; // Slightly more than one buffer size
+ InputStream input = randomDataToInputStream(bytes);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
+
+ try (final ImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
+ stream.setByteOrder(ByteOrder.BIG_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 4; i++) {
+ assertEquals(buffer.getInt(), stream.readInt());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readInt();
+ }
+ });
+
+ stream.seek(0);
+ stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ buffer.position(0);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 4; i++) {
+ assertEquals(buffer.getInt(), stream.readInt());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readInt();
+ }
+ });
+ }
+ }
+
+ @Test
+ public void testReadLong() throws IOException {
+ byte[] bytes = new byte[8743]; // Slightly more than one buffer size
+ InputStream input = randomDataToInputStream(bytes);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
+
+ try (final ImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
+ stream.setByteOrder(ByteOrder.BIG_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 8; i++) {
+ assertEquals(buffer.getLong(), stream.readLong());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readLong();
+ }
+ });
+
+ stream.seek(0);
+ stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ buffer.position(0);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 8; i++) {
+ assertEquals(buffer.getLong(), stream.readLong());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readLong();
+ }
+ });
+ }
+ }
+
+ @Test
+ public void testSeekPastEOF() throws IOException {
+ byte[] bytes = new byte[9];
+ InputStream input = randomDataToInputStream(bytes);
+
+ try (final ImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
+ stream.seek(1000);
+
+ assertEquals(-1, stream.read());
+ assertEquals(-1, stream.read(new byte[1], 0, 1));
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readFully(new byte[1]);
+ }
+ });
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readByte();
+ }
+ });
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readShort();
+ }
+ });
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readInt();
+ }
+ });
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readLong();
+ }
+ });
+
+ stream.seek(0);
+ for (byte value : bytes) {
+ assertEquals(value, stream.readByte());
+ }
+
+ assertEquals(-1, stream.read());
+ }
+ }
+
+ @Test
+ public void testClose() throws IOException {
+ // Create wrapper stream
+ InputStream mock = mock(InputStream.class);
+ ImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(mock));
+
+ stream.close();
+ verify(mock, only()).close();
+ }
+
+ @Test
+ public void testWorkaroundForWBMPImageReaderExpectsReadToBehaveAsReadFully() throws IOException {
+ // See #606 for details.
+ // Bug in JDK WBMPImageReader, uses read(byte[], int, int) instead of readFully(byte[], int, int).
+ // Ie: Relies on read to return all bytes at once, without blocking
+ int size = BufferedChannelImageInputStream.DEFAULT_BUFFER_SIZE * 7;
+ byte[] bytes = new byte[size];
+ InputStream input = randomDataToInputStream(bytes);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
+ byte[] result = new byte[size];
+ int head = stream.read(result, 0, 12); // Provoke a buffered read
+ int len = stream.read(result, 12, size - 12); // Rest of buffer + direct read
+
+ assertEquals(size, len + head);
+ assertArrayEquals(bytes, result);
+ }
+ }
+}
diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedChannelImageInputStreamTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedChannelImageInputStreamTest.java
new file mode 100755
index 00000000..a38dcfcb
--- /dev/null
+++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedChannelImageInputStreamTest.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (c) 2020, 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 of the copyright holder 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 HOLDER 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.imageio.stream;
+
+import org.junit.Test;
+import org.junit.function.ThrowingRunnable;
+
+import javax.imageio.stream.ImageInputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.Files;
+import java.util.Random;
+
+import static com.twelvemonkeys.imageio.stream.BufferedImageInputStreamTest.rangeEquals;
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * BufferedFileImageInputStreamTestCase
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: BufferedFileImageInputStreamTestCase.java,v 1.0 Apr 21, 2009 10:58:48 AM haraldk Exp$
+ */
+public class BufferedChannelImageInputStreamTest {
+ private final Random random = new Random(170984354357234566L);
+
+ private File randomDataToFile(byte[] data) throws IOException {
+ random.nextBytes(data);
+
+ File file = File.createTempFile("read", ".tmp");
+ Files.write(file.toPath(), data);
+ return file;
+ }
+
+ @Test
+ public void testCreate() throws IOException {
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(File.createTempFile("empty", ".tmp")))) {
+ assertEquals("Data length should be same as stream length", 0, stream.length());
+ }
+ }
+
+ @Test
+ public void testCreateNullFileInputStream() {
+ try {
+ new BufferedChannelImageInputStream((FileInputStream) null);
+ fail("Expected IllegalArgumentException");
+ }
+ catch (IllegalArgumentException expected) {
+ assertNotNull("Null exception message", expected.getMessage());
+ String message = expected.getMessage().toLowerCase();
+ assertTrue("Exception message does not contain parameter name", message.contains("inputstream"));
+ assertTrue("Exception message does not contain null", message.contains("null"));
+ }
+ }
+
+ @Test
+ public void testCreateNullByteChannel() {
+ try {
+ new BufferedChannelImageInputStream((SeekableByteChannel) null);
+ fail("Expected IllegalArgumentException");
+ }
+ catch (IllegalArgumentException expected) {
+ assertNotNull("Null exception message", expected.getMessage());
+ String message = expected.getMessage().toLowerCase();
+ assertTrue("Exception message does not contain parameter name", message.contains("channel"));
+ assertTrue("Exception message does not contain null", message.contains("null"));
+ }
+ }
+
+ @Test
+ public void testRead() throws IOException {
+ byte[] data = new byte[1024 * 1024];
+ File file = randomDataToFile(data);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
+ assertEquals("File length should be same as stream length", file.length(), stream.length());
+
+ for (byte value : data) {
+ assertEquals("Wrong data read", value & 0xff, stream.read());
+ }
+ }
+ }
+
+ @Test
+ public void testReadArray() throws IOException {
+ byte[] data = new byte[1024 * 1024];
+ File file = randomDataToFile(data);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
+ assertEquals("File length should be same as stream length", file.length(), stream.length());
+
+ byte[] result = new byte[1024];
+
+ for (int i = 0; i < data.length / result.length; i++) {
+ stream.readFully(result);
+ assertTrue("Wrong data read: " + i, rangeEquals(data, i * result.length, result, 0, result.length));
+ }
+ }
+ }
+
+ @Test
+ public void testReadSkip() throws IOException {
+ byte[] data = new byte[1024 * 14];
+ File file = randomDataToFile(data);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
+ assertEquals("File length should be same as stream length", file.length(), stream.length());
+
+ byte[] result = new byte[7];
+
+ for (int i = 0; i < data.length / result.length; i += 2) {
+ stream.readFully(result);
+ stream.skipBytes(result.length);
+ assertTrue("Wrong data read: " + i, rangeEquals(data, i * result.length, result, 0, result.length));
+ }
+ }
+ }
+
+ @Test
+ public void testReadSeek() throws IOException {
+ byte[] data = new byte[1024 * 18];
+ File file = randomDataToFile(data);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
+ assertEquals("File length should be same as stream length", file.length(), stream.length());
+
+ byte[] result = new byte[9];
+
+ for (int i = 0; i < data.length / result.length; i++) {
+ // Read backwards
+ long newPos = stream.length() - result.length - i * result.length;
+ stream.seek(newPos);
+ assertEquals("Wrong stream position", newPos, stream.getStreamPosition());
+ stream.readFully(result);
+ assertTrue("Wrong data read: " + i, rangeEquals(data, (int) newPos, result, 0, result.length));
+ }
+ }
+ }
+
+ @Test
+ public void testReadOutsideDataSeek0Read() throws IOException {
+ byte[] data = new byte[256];
+ File file = randomDataToFile(data);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
+ assertEquals("File length should be same as stream length", file.length(), stream.length());
+
+ byte[] buffer = new byte[data.length * 2];
+ stream.read(buffer);
+ stream.seek(0);
+ assertNotEquals(-1, stream.read());
+ assertNotEquals(-1, stream.read(buffer));
+ }
+ }
+
+ @Test
+ public void testReadBitRandom() throws IOException {
+ byte[] bytes = new byte[8];
+ File file = randomDataToFile(bytes);
+ long value = ByteBuffer.wrap(bytes).getLong();
+
+ // Create stream
+ try (ImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
+ for (int i = 1; i <= 64; i++) {
+ assertEquals(String.format("bit %d differ", i), (value << (i - 1L)) >>> 63L, stream.readBit());
+ }
+ }
+ }
+
+ @Test
+ public void testReadBitsRandom() throws IOException {
+ byte[] bytes = new byte[8];
+ File file = randomDataToFile(bytes);
+ long value = ByteBuffer.wrap(bytes).getLong();
+
+ // Create stream
+ try (ImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
+ for (int i = 1; i <= 64; i++) {
+ stream.seek(0);
+ assertEquals(String.format("bit %d differ", i), value >>> (64L - i), stream.readBits(i));
+ assertEquals(i % 8, stream.getBitOffset());
+ }
+ }
+ }
+
+ @Test
+ public void testReadBitsRandomOffset() throws IOException {
+ byte[] bytes = new byte[8];
+ File file = randomDataToFile(bytes);
+ long value = ByteBuffer.wrap(bytes).getLong();
+
+ // Create stream
+ try (ImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
+ for (int i = 1; i <= 60; i++) {
+ stream.seek(0);
+ stream.setBitOffset(i % 8);
+ assertEquals(String.format("bit %d differ", i), (value << (i % 8)) >>> (64L - i), stream.readBits(i));
+ assertEquals(i * 2 % 8, stream.getBitOffset());
+ }
+ }
+ }
+
+ @Test
+ public void testReadShort() throws IOException {
+ byte[] bytes = new byte[8743]; // Slightly more than one buffer size
+ File file = randomDataToFile(bytes);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
+
+ try (final ImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
+ stream.setByteOrder(ByteOrder.BIG_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 2; i++) {
+ assertEquals(buffer.getShort(), stream.readShort());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readShort();
+ }
+ });
+
+ stream.seek(0);
+ stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ buffer.position(0);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 2; i++) {
+ assertEquals(buffer.getShort(), stream.readShort());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readShort();
+ }
+ });
+ }
+ }
+
+ @Test
+ public void testReadInt() throws IOException {
+ byte[] bytes = new byte[8743]; // Slightly more than one buffer size
+ File file = randomDataToFile(bytes);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
+
+ try (final ImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
+ stream.setByteOrder(ByteOrder.BIG_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 4; i++) {
+ assertEquals(buffer.getInt(), stream.readInt());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readInt();
+ }
+ });
+
+ stream.seek(0);
+ stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ buffer.position(0);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 4; i++) {
+ assertEquals(buffer.getInt(), stream.readInt());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readInt();
+ }
+ });
+ }
+ }
+
+ @Test
+ public void testReadLong() throws IOException {
+ byte[] bytes = new byte[8743]; // Slightly more than one buffer size
+ File file = randomDataToFile(bytes);
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
+
+ try (final ImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
+ stream.setByteOrder(ByteOrder.BIG_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 8; i++) {
+ assertEquals(buffer.getLong(), stream.readLong());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readLong();
+ }
+ });
+
+ stream.seek(0);
+ stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ buffer.position(0);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+
+ for (int i = 0; i < bytes.length / 8; i++) {
+ assertEquals(buffer.getLong(), stream.readLong());
+ }
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readLong();
+ }
+ });
+ }
+ }
+
+ @Test
+ public void testSeekPastEOF() throws IOException {
+ byte[] bytes = new byte[9];
+ File file = randomDataToFile(bytes);
+
+ try (final ImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
+ stream.seek(1000);
+
+ assertEquals(-1, stream.read());
+ assertEquals(-1, stream.read(new byte[1], 0, 1));
+
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readFully(new byte[1]);
+ }
+ });
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readByte();
+ }
+ });
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readShort();
+ }
+ });
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readInt();
+ }
+ });
+ assertThrows(EOFException.class, new ThrowingRunnable() {
+ @Override
+ public void run() throws Throwable {
+ stream.readLong();
+ }
+ });
+
+ stream.seek(0);
+ for (byte value : bytes) {
+ assertEquals(value, stream.readByte());
+ }
+
+ assertEquals(-1, stream.read());
+ }
+ }
+
+ @Test
+ public void testCloseStream() throws IOException {
+ // Create wrapper stream
+ FileInputStream mock = mock(FileInputStream.class);
+ when(mock.getChannel()).thenCallRealMethod();
+ ImageInputStream stream = new BufferedChannelImageInputStream(mock);
+ reset(mock);
+
+ stream.close();
+ verify(mock, only()).close();
+ }
+
+ @Test
+ public void testCloseChannel() throws IOException {
+ // Create wrapper stream
+ SeekableByteChannel mock = mock(SeekableByteChannel.class);
+ ImageInputStream stream = new BufferedChannelImageInputStream(mock);
+
+ stream.close();
+ verify(mock, only()).close();
+ }
+
+ @Test
+ public void testWorkaroundForWBMPImageReaderExpectsReadToBehaveAsReadFully() throws IOException {
+ // See #606 for details.
+ // Bug in JDK WBMPImageReader, uses read(byte[], int, int) instead of readFully(byte[], int, int).
+ // Ie: Relies on read to return all bytes at once, without blocking
+ int size = BufferedChannelImageInputStream.DEFAULT_BUFFER_SIZE * 7;
+ byte[] bytes = new byte[size];
+ File file = randomDataToFile(bytes);
+
+ try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
+ byte[] result = new byte[size];
+ int head = stream.read(result, 0, 12); // Provoke a buffered read
+ int len = stream.read(result, 12, size - 12); // Rest of buffer + direct read
+
+ assertEquals(size, len + head);
+ assertArrayEquals(bytes, result);
+ }
+ }
+}
diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamTest.java
index 0d4b2d12..12fe2746 100755
--- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamTest.java
+++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamTest.java
@@ -45,7 +45,9 @@ import java.util.Random;
import static com.twelvemonkeys.imageio.stream.BufferedImageInputStreamTest.rangeEquals;
import static org.junit.Assert.*;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.verify;
/**
* BufferedFileImageInputStreamTestCase
@@ -54,6 +56,7 @@ import static org.mockito.Mockito.*;
* @author last modified by $Author: haraldk$
* @version $Id: BufferedFileImageInputStreamTestCase.java,v 1.0 Apr 21, 2009 10:58:48 AM haraldk Exp$
*/
+@Deprecated
public class BufferedFileImageInputStreamTest {
private final Random random = new Random(170984354357234566L);
@@ -72,6 +75,7 @@ public class BufferedFileImageInputStreamTest {
}
}
+ @SuppressWarnings("resource")
@Test
public void testCreateNullFile() throws IOException {
try {
diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileInputStreamImageInputStreamSpiTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileInputStreamImageInputStreamSpiTest.java
new file mode 100644
index 00000000..6e0e0597
--- /dev/null
+++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileInputStreamImageInputStreamSpiTest.java
@@ -0,0 +1,26 @@
+package com.twelvemonkeys.imageio.stream;
+
+import javax.imageio.spi.ImageInputStreamSpi;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+
+/**
+ * BufferedInputStreamImageInputStreamSpiTest.
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: BufferedInputStreamImageInputStreamSpiTest.java,v 1.0 08/09/2022 haraldk Exp$
+ */
+public class BufferedFileInputStreamImageInputStreamSpiTest extends ImageInputStreamSpiTest {
+ @Override
+ protected ImageInputStreamSpi createProvider() {
+ return new BufferedInputStreamImageInputStreamSpi();
+ }
+
+ @Override
+ protected InputStream createInput() throws IOException {
+ return Files.newInputStream(File.createTempFile("test-", ".tst").toPath());
+ }
+}
\ No newline at end of file
diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedInputStreamImageInputStreamSpiTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedInputStreamImageInputStreamSpiTest.java
new file mode 100644
index 00000000..b8f117a2
--- /dev/null
+++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedInputStreamImageInputStreamSpiTest.java
@@ -0,0 +1,24 @@
+package com.twelvemonkeys.imageio.stream;
+
+import javax.imageio.spi.ImageInputStreamSpi;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+/**
+ * BufferedInputStreamImageInputStreamSpiTest.
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: BufferedInputStreamImageInputStreamSpiTest.java,v 1.0 08/09/2022 haraldk Exp$
+ */
+public class BufferedInputStreamImageInputStreamSpiTest extends ImageInputStreamSpiTest {
+ @Override
+ protected ImageInputStreamSpi createProvider() {
+ return new BufferedInputStreamImageInputStreamSpi();
+ }
+
+ @Override
+ protected InputStream createInput() {
+ return new ByteArrayInputStream(new byte[0]);
+ }
+}
\ No newline at end of file