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