Merge branch 'master' into webp

This commit is contained in:
Harald Kuhr
2021-03-27 14:44:23 +01:00
83 changed files with 3396 additions and 1175 deletions

View File

@@ -104,6 +104,6 @@
</dependencies>
<properties>
<batik.version>1.12</batik.version>
<batik.version>1.14</batik.version>
</properties>
</project>

View File

@@ -655,7 +655,7 @@ public class SVGImageReader extends ImageReaderBase {
if (allowExternalResources) {
return super.getExternalResourceSecurity(resourceURL, docURL);
}
return new NoLoadExternalResourceSecurity();
return new EmbededExternalResourceSecurity(resourceURL);
}
}
}

View File

@@ -297,6 +297,25 @@ public class SVGImageReaderTest extends ImageReaderAbstractTest<SVGImageReader>
}
}
@Test
public void testReadEmbeddedWithDisallowExternalResources() throws IOException{
// File using "data:" URLs for embedded resources
URL resource = getClassLoaderResource("/svg/embedded-data-resource.svg");
SVGImageReader reader = createReader();
TestData data = new TestData(resource, (Dimension) null);
try (ImageInputStream stream = data.getInputStream()) {
reader.setInput(stream);
SVGReadParam param = reader.getDefaultReadParam();
param.setAllowExternalResources(false);
reader.read(0, param);
}
finally {
reader.dispose();
}
}
@Test(expected = SecurityException.class)
public void testDisallowedExternalResources() throws URISyntaxException, IOException {
// system-property set to true in surefire-plugin-settings in the pom

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 92 KiB

View File

@@ -56,6 +56,8 @@ import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.List;
@@ -131,7 +133,13 @@ public final class Paths {
List<JPEGSegment> photoshop = JPEGSegmentUtil.readSegments(stream, segmentIdentifiers);
if (!photoshop.isEmpty()) {
return readPathFromPhotoshopResources(new MemoryCacheImageInputStream(photoshop.get(0).data()));
InputStream data = null;
for (JPEGSegment ps : photoshop) {
data = data == null ? ps.data() : new SequenceInputStream(data, ps.data());
}
return readPathFromPhotoshopResources(new MemoryCacheImageInputStream(data));
}
}
else if (magic >>> 16 == TIFF.BYTE_ORDER_MARK_BIG_ENDIAN && (magic & 0xffff) == TIFF.TIFF_MAGIC
@@ -350,10 +358,10 @@ public final class Paths {
IIOMetadataNode unknown = new IIOMetadataNode("unknown");
unknown.setAttribute("MarkerTag", Integer.toString(JPEG.APP13 & 0xFF));
byte[] identfier = "Photoshop 3.0".getBytes(StandardCharsets.US_ASCII);
byte[] data = new byte[identfier.length + 1 + pathResource.length];
System.arraycopy(identfier, 0, data, 0, identfier.length);
System.arraycopy(pathResource, 0, data, identfier.length + 1, pathResource.length);
byte[] identifier = "Photoshop 3.0".getBytes(StandardCharsets.US_ASCII);
byte[] data = new byte[identifier.length + 1 + pathResource.length];
System.arraycopy(identifier, 0, data, 0, identifier.length);
System.arraycopy(pathResource, 0, data, identifier.length + 1, pathResource.length);
unknown.setUserObject(data);

View File

@@ -30,16 +30,20 @@
package com.twelvemonkeys.imageio;
import com.twelvemonkeys.image.BufferedImageIcon;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.io.File;
@@ -48,20 +52,6 @@ import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Iterator;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import javax.swing.*;
import com.twelvemonkeys.image.BufferedImageIcon;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.imageio.util.IIOUtil;
/**
* Abstract base class for image readers.
*
@@ -275,8 +265,9 @@ public abstract class ImageReaderBase extends ImageReader {
// - transferType is ok
// - bands are ok
// TODO: Test if color model is ok?
if (specifier.getSampleModel().getTransferType() == dest.getSampleModel().getTransferType() &&
specifier.getNumBands() <= dest.getSampleModel().getNumBands()) {
if (specifier.getSampleModel().getTransferType() == dest.getSampleModel().getTransferType()
&& Arrays.equals(specifier.getSampleModel().getSampleSize(), dest.getSampleModel().getSampleSize())
&& specifier.getNumBands() <= dest.getSampleModel().getNumBands()) {
found = true;
break;
}
@@ -450,6 +441,7 @@ public abstract class ImageReaderBase extends ImageReader {
static final String ZOOM_IN = "zoom-in";
static final String ZOOM_OUT = "zoom-out";
static final String ZOOM_ACTUAL = "zoom-actual";
static final String ZOOM_FIT = "zoom-fit";
private BufferedImage image;
@@ -525,9 +517,20 @@ public abstract class ImageReaderBase extends ImageReader {
private void setupActions() {
// Mac weirdness... VK_MINUS/VK_PLUS seems to map to english key map always...
bindAction(new ZoomAction("Zoom in", 2), ZOOM_IN, KeyStroke.getKeyStroke('+'), KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0));
bindAction(new ZoomAction("Zoom out", .5), ZOOM_OUT, KeyStroke.getKeyStroke('-'), KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0));
bindAction(new ZoomAction("Zoom actual"), ZOOM_ACTUAL, KeyStroke.getKeyStroke('0'), KeyStroke.getKeyStroke(KeyEvent.VK_0, 0));
bindAction(new ZoomAction("Zoom in", 2), ZOOM_IN,
KeyStroke.getKeyStroke('+'),
KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()),
KeyStroke.getKeyStroke(KeyEvent.VK_ADD, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
bindAction(new ZoomAction("Zoom out", .5), ZOOM_OUT,
KeyStroke.getKeyStroke('-'),
KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()),
KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
bindAction(new ZoomAction("Zoom actual"), ZOOM_ACTUAL,
KeyStroke.getKeyStroke('0'),
KeyStroke.getKeyStroke(KeyEvent.VK_0, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
bindAction(new ZoomToFitAction("Zoom fit"), ZOOM_FIT,
KeyStroke.getKeyStroke('9'),
KeyStroke.getKeyStroke(KeyEvent.VK_9, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
bindAction(TransferHandler.getCopyAction(), (String) TransferHandler.getCopyAction().getValue(Action.NAME), KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
bindAction(TransferHandler.getPasteAction(), (String) TransferHandler.getPasteAction().getValue(Action.NAME), KeyStroke.getKeyStroke(KeyEvent.VK_V, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
@@ -544,6 +547,7 @@ public abstract class ImageReaderBase extends ImageReader {
private JPopupMenu createPopupMenu() {
JPopupMenu popup = new JPopupMenu();
popup.add(getActionMap().get(ZOOM_FIT));
popup.add(getActionMap().get(ZOOM_ACTUAL));
popup.add(getActionMap().get(ZOOM_IN));
popup.add(getActionMap().get(ZOOM_OUT));
@@ -564,7 +568,7 @@ public abstract class ImageReaderBase extends ImageReader {
addCheckBoxItem(new ChangeBackgroundAction("Dark", Color.DARK_GRAY), background, group);
addCheckBoxItem(new ChangeBackgroundAction("Black", Color.BLACK), background, group);
background.addSeparator();
ChooseBackgroundAction chooseBackgroundAction = new ChooseBackgroundAction("Choose...", defaultBG != null ? defaultBG : Color.BLUE);
ChooseBackgroundAction chooseBackgroundAction = new ChooseBackgroundAction("Choose...", defaultBG != null ? defaultBG : new Color(0xFF6600));
chooseBackgroundAction.putValue(Action.SELECTED_KEY, backgroundPaint == defaultBG);
addCheckBoxItem(chooseBackgroundAction, background, group);
@@ -678,14 +682,41 @@ public abstract class ImageReaderBase extends ImageReader {
}
else {
Icon current = getIcon();
int w = (int) Math.max(Math.min(current.getIconWidth() * zoomFactor, image.getWidth() * 16), image.getWidth() / 16);
int h = (int) Math.max(Math.min(current.getIconHeight() * zoomFactor, image.getHeight() * 16), image.getHeight() / 16);
int w = Math.max(Math.min((int) (current.getIconWidth() * zoomFactor), image.getWidth() * 16), image.getWidth() / 16);
int h = Math.max(Math.min((int) (current.getIconHeight() * zoomFactor), image.getHeight() * 16), image.getHeight() / 16);
setIcon(new BufferedImageIcon(image, Math.max(w, 2), Math.max(h, 2), w > image.getWidth() || h > image.getHeight()));
}
}
}
private class ZoomToFitAction extends ZoomAction {
public ZoomToFitAction(final String name) {
super(name, -1);
}
public void actionPerformed(final ActionEvent e) {
JComponent source = (JComponent) e.getSource();
if (source instanceof JMenuItem) {
JPopupMenu menu = (JPopupMenu) SwingUtilities.getAncestorOfClass(JPopupMenu.class, source);
source = (JComponent) menu.getInvoker();
}
Container container = SwingUtilities.getAncestorOfClass(JViewport.class, source);
double ratioX = container.getWidth() / (double) image.getWidth();
double ratioY = container.getHeight() / (double) image.getHeight();
double zoomFactor = Math.min(ratioX, ratioY);
int w = Math.max(Math.min((int) (image.getWidth() * zoomFactor), image.getWidth() * 16), image.getWidth() / 16);
int h = Math.max(Math.min((int) (image.getHeight() * zoomFactor), image.getHeight() * 16), image.getHeight() / 16);
setIcon(new BufferedImageIcon(image, w, h, zoomFactor > 1));
}
}
private static class ImageTransferable implements Transferable {
private final BufferedImage image;
@@ -704,7 +735,7 @@ public abstract class ImageReaderBase extends ImageReader {
}
@Override
public Object getTransferData(final DataFlavor flavor) throws UnsupportedFlavorException, IOException {
public Object getTransferData(final DataFlavor flavor) throws UnsupportedFlavorException {
if (isDataFlavorSupported(flavor)) {
return image;
}

View File

@@ -0,0 +1,251 @@
/*
* Copyright (c) 2021, 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.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import static com.twelvemonkeys.lang.Validate.notNull;
import static java.lang.Math.max;
/**
* A buffered replacement for {@link javax.imageio.stream.FileImageInputStream}
* that provides greatly improved performance for shorter reads, like single
* byte or bit reads.
* As with {@code javax.imageio.stream.FileImageInputStream}, either
* {@link File} or {@link RandomAccessFile} can be used as input.
*
* @see javax.imageio.stream.FileImageInputStream
*/
// TODO: Create a memory-mapped version?
// Or not... 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.
public final class BufferedFileImageInputStream extends ImageInputStreamImpl {
static final int DEFAULT_BUFFER_SIZE = 8192;
private byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
private int bufferPos;
private int bufferLimit;
private final ByteBuffer integralCache = ByteBuffer.allocate(8);
private final byte[] integralCacheArray = integralCache.array();
private RandomAccessFile raf;
/**
* Constructs a <code>BufferedFileImageInputStream</code> that will read from a given <code>File</code>.
*
* @param file a <code>File</code> to read from.
* @throws IllegalArgumentException if <code>file</code> is <code>null</code>.
* @throws FileNotFoundException if <code>file</code> is a directory or cannot be opened for reading
* for any reason.
*/
public BufferedFileImageInputStream(final File file) throws FileNotFoundException {
this(new RandomAccessFile(notNull(file, "file"), "r"));
}
/**
* Constructs a <code>BufferedFileImageInputStream</code> that will read from a given <code>RandomAccessFile</code>.
*
* @param raf a <code>RandomAccessFile</code> to read from.
* @throws IllegalArgumentException if <code>raf</code> is <code>null</code>.
*/
public BufferedFileImageInputStream(final RandomAccessFile raf) {
this.raf = notNull(raf, "raf");
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean fillBuffer() throws IOException {
bufferPos = 0;
int length = raf.read(buffer, 0, buffer.length);
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[] pBuffer, final int pOffset, final int pLength) throws IOException {
checkClosed();
bitOffset = 0;
if (bufferEmpty()) {
// Bypass buffer if buffer is empty for reads longer than buffer
if (pLength >= buffer.length) {
return readDirect(pBuffer, pOffset, pLength);
}
else if (!fillBuffer()) {
return -1;
}
}
return readBuffered(pBuffer, pOffset, pLength);
}
private int readDirect(final byte[] pBuffer, final int pOffset, final int pLength) throws IOException {
// Invalidate the buffer, as its contents is no longer in sync with the stream's position.
bufferLimit = 0;
int read = raf.read(pBuffer, pOffset, pLength);
if (read > 0) {
streamPos += read;
}
return read;
}
private int readBuffered(final byte[] pBuffer, final int pOffset, final int pLength) {
// Read as much as possible from buffer
int length = Math.min(bufferLimit - bufferPos, pLength);
if (length > 0) {
System.arraycopy(buffer, bufferPos, pBuffer, pOffset, length);
bufferPos += length;
streamPos += length;
}
return length;
}
public long length() {
// WTF?! This method is allowed to throw IOException in the interface...
try {
checkClosed();
return raf.length();
}
catch (IOException ignore) {
}
return -1;
}
public void close() throws IOException {
super.close();
raf.close();
raf = null;
buffer = 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;
raf.seek(position);
}
streamPos = position;
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright (c) 2021, 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 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.FileNotFoundException;
import java.util.Iterator;
import java.util.Locale;
/**
* BufferedFileImageInputStreamSpi
* Experimental
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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 BufferedFileImageInputStreamSpi() {
this(new StreamProviderInfo());
}
private BufferedFileImageInputStreamSpi(ProviderInfo providerInfo) {
super(providerInfo.getVendorName(), providerInfo.getVersion(), File.class);
}
@Override
public void onRegistration(final ServiceRegistry registry, final Class<?> category) {
Iterator<ImageInputStreamSpi> providers = registry.getServiceProviders(ImageInputStreamSpi.class, new FileInputFilter(), true);
while (providers.hasNext()) {
ImageInputStreamSpi provider = providers.next();
if (provider != this) {
registry.setOrdering(ImageInputStreamSpi.class, this, provider);
}
}
}
public ImageInputStream createInputStreamInstance(final Object input, final boolean pUseCache, final File pCacheDir) {
if (input instanceof File) {
try {
return new BufferedFileImageInputStream((File) input);
}
catch (FileNotFoundException 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,
// instead they should be allowed to inspect the instance, to see that the file does exist...
return null;
}
}
throw new IllegalArgumentException("Expected input of type File: " + input);
}
@Override
public boolean canUseCacheFile() {
return false;
}
public String getDescription(final Locale pLocale) {
return "Service provider that instantiates an ImageInputStream from a File";
}
private static class FileInputFilter implements ServiceRegistry.Filter {
@Override
public boolean filter(final Object provider) {
return ((ImageInputStreamSpi) provider).getInputClass() == File.class;
}
}
}

View File

@@ -43,15 +43,18 @@ import static com.twelvemonkeys.lang.Validate.notNull;
* A buffered {@code ImageInputStream}.
* Experimental - seems to be effective for {@link javax.imageio.stream.FileImageInputStream}
* and {@link javax.imageio.stream.FileCacheImageInputStream} when doing a lot of single-byte reads
* (or short byte-array reads) on OS X at least.
* (or short byte-array reads).
* Code that uses the {@code readFully} methods are not affected by the issue.
* <p/>
* NOTE: Invoking {@code close()} will <em>NOT</em> close the wrapped stream.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: BufferedFileImageInputStream.java,v 1.0 May 15, 2008 4:36:49 PM haraldk Exp$
*
* @deprecated Use {@link BufferedFileImageInputStream} instead.
*/
// TODO: Create a provider for this (wrapping the FileIIS and FileCacheIIS classes), and disable the Sun built-in spis?
// TODO: Test on other platforms, might be just an OS X issue
@Deprecated
public final class BufferedImageInputStream extends ImageInputStreamImpl implements ImageInputStream {
static final int DEFAULT_BUFFER_SIZE = 8192;
@@ -255,6 +258,7 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme
}
int val = buffer.get() & 0xff;
streamPos++;
accum <<= 8;
accum |= val;
@@ -264,9 +268,7 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme
// Move byte position back if in the middle of a byte
if (newBitOffset != 0) {
buffer.position(buffer.position() - 1);
}
else {
streamPos++;
streamPos--;
}
this.bitOffset = newBitOffset;
@@ -281,26 +283,26 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme
}
@Override
public void seek(long pPosition) throws IOException {
public void seek(long position) throws IOException {
checkClosed();
bitOffset = 0;
if (streamPos == pPosition) {
if (streamPos == position) {
return;
}
// Optimized to not invalidate buffer if new position is within current buffer
long newBufferPos = buffer.position() + pPosition - streamPos;
long newBufferPos = buffer.position() + position - streamPos;
if (newBufferPos >= 0 && newBufferPos <= buffer.limit()) {
buffer.position((int) newBufferPos);
}
else {
// Will invalidate buffer
buffer.limit(0);
stream.seek(pPosition);
stream.seek(position);
}
streamPos = pPosition;
streamPos = position;
}
@Override
@@ -332,7 +334,9 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme
@Override
public void close() throws IOException {
if (stream != null) {
//stream.close();
// TODO: FixMe: Need to close underlying stream here!
// For call sites that relies on not closing, we should instead not close the buffered stream.
// stream.close();
stream = null;
buffer = null;
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright (c) 2021, 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 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.RandomAccessFile;
import java.util.Iterator;
import java.util.Locale;
/**
* BufferedRAFImageInputStreamSpi
* Experimental
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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 BufferedRAFImageInputStreamSpi() {
this(new StreamProviderInfo());
}
private BufferedRAFImageInputStreamSpi(ProviderInfo providerInfo) {
super(providerInfo.getVendorName(), providerInfo.getVersion(), RandomAccessFile.class);
}
@Override
public void onRegistration(final ServiceRegistry registry, final Class<?> category) {
Iterator<ImageInputStreamSpi> providers = registry.getServiceProviders(ImageInputStreamSpi.class, new RAFInputFilter(), true);
while (providers.hasNext()) {
ImageInputStreamSpi provider = providers.next();
if (provider != this) {
registry.setOrdering(ImageInputStreamSpi.class, this, provider);
}
}
}
public ImageInputStream createInputStreamInstance(final Object input, final boolean pUseCache, final File pCacheDir) {
if (input instanceof RandomAccessFile) {
return new BufferedFileImageInputStream((RandomAccessFile) input);
}
throw new IllegalArgumentException("Expected input of type RandomAccessFile: " + input);
}
@Override
public boolean canUseCacheFile() {
return false;
}
public String getDescription(final Locale pLocale) {
return "Service provider that instantiates an ImageInputStream from a RandomAccessFile";
}
private static class RAFInputFilter implements ServiceRegistry.Filter {
@Override
public boolean filter(final Object provider) {
return ((ImageInputStreamSpi) provider).getInputClass() == RandomAccessFile.class;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2008, Harald Kuhr
* Copyright (c) 2021, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -34,7 +34,6 @@ import com.twelvemonkeys.imageio.spi.ProviderInfo;
import javax.imageio.spi.ImageInputStreamSpi;
import javax.imageio.stream.FileCacheImageInputStream;
import javax.imageio.stream.FileImageInputStream;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import java.io.File;
@@ -72,7 +71,7 @@ public class URLImageInputStreamSpi extends ImageInputStreamSpi {
// Special case for file protocol, a lot faster than FileCacheImageInputStream
if ("file".equals(url.getProtocol())) {
try {
return new BufferedImageInputStream(new FileImageInputStream(new File(url.toURI())));
return new BufferedFileImageInputStream(new File(url.toURI()));
}
catch (URISyntaxException ignore) {
// This should never happen, but if it does, we'll fall back to using the stream
@@ -81,29 +80,29 @@ public class URLImageInputStreamSpi extends ImageInputStreamSpi {
}
// Otherwise revert to cached
final InputStream stream = url.openStream();
final InputStream urlStream = url.openStream();
if (pUseCache) {
return new BufferedImageInputStream(new FileCacheImageInputStream(stream, pCacheDir) {
return new FileCacheImageInputStream(urlStream, pCacheDir) {
@Override
public void close() throws IOException {
try {
super.close();
}
finally {
stream.close(); // NOTE: If this line throws IOE, it will shadow the original..
urlStream.close(); // NOTE: If this line throws IOE, it will shadow the original..
}
}
});
};
}
else {
return new MemoryCacheImageInputStream(stream) {
return new MemoryCacheImageInputStream(urlStream) {
@Override
public void close() throws IOException {
try {
super.close();
}
finally {
stream.close(); // NOTE: If this line throws IOE, it will shadow the original..
urlStream.close(); // NOTE: If this line throws IOE, it will shadow the original..
}
}
};

View File

@@ -223,7 +223,12 @@ public final class IIOUtil {
public static void subsampleRow(byte[] srcRow, int srcPos, int srcWidth,
byte[] destRow, int destPos,
int samplesPerPixel, int bitsPerSample, int samplePeriod) {
Validate.isTrue(samplePeriod > 1, "samplePeriod must be > 1"); // Period == 1 could be a no-op...
// Period == 1 is a no-op...
if (samplePeriod == 1) {
return;
}
Validate.isTrue(samplePeriod > 1, "samplePeriod must be > 1");
Validate.isTrue(bitsPerSample > 0 && bitsPerSample <= 8 && (bitsPerSample == 1 || bitsPerSample % 2 == 0),
"bitsPerSample must be > 0 and <= 8 and a power of 2");
Validate.isTrue(samplesPerPixel > 0, "samplesPerPixel must be > 0");
@@ -261,7 +266,12 @@ public final class IIOUtil {
public static void subsampleRow(short[] srcRow, int srcPos, int srcWidth,
short[] destRow, int destPos,
int samplesPerPixel, int bitsPerSample, int samplePeriod) {
Validate.isTrue(samplePeriod > 1, "samplePeriod must be > 1"); // Period == 1 could be a no-op...
// Period == 1 is a no-op...
if (samplePeriod == 1) {
return;
}
Validate.isTrue(samplePeriod > 1, "samplePeriod must be > 1");
Validate.isTrue(bitsPerSample > 0 && bitsPerSample <= 16 && (bitsPerSample == 1 || bitsPerSample % 2 == 0),
"bitsPerSample must be > 0 and <= 16 and a power of 2");
Validate.isTrue(samplesPerPixel > 0, "samplesPerPixel must be > 0");
@@ -278,7 +288,12 @@ public final class IIOUtil {
public static void subsampleRow(int[] srcRow, int srcPos, int srcWidth,
int[] destRow, int destPos,
int samplesPerPixel, int bitsPerSample, int samplePeriod) {
Validate.isTrue(samplePeriod > 1, "samplePeriod must be > 1"); // Period == 1 could be a no-op...
// Period == 1 is a no-op...
if (samplePeriod == 1) {
return;
}
Validate.isTrue(samplePeriod > 1, "samplePeriod must be > 1");
Validate.isTrue(bitsPerSample > 0 && bitsPerSample <= 32 && (bitsPerSample == 1 || bitsPerSample % 2 == 0),
"bitsPerSample must be > 0 and <= 32 and a power of 2");
Validate.isTrue(samplesPerPixel > 0, "samplesPerPixel must be > 0");

View File

@@ -0,0 +1,2 @@
com.twelvemonkeys.imageio.stream.BufferedFileImageInputStreamSpi
com.twelvemonkeys.imageio.stream.BufferedRAFImageInputStreamSpi

View File

@@ -0,0 +1,30 @@
package com.twelvemonkeys.imageio.stream;
import org.junit.Test;
import javax.imageio.spi.ImageInputStreamSpi;
import java.io.File;
import java.io.IOException;
import static org.junit.Assert.assertNull;
import static org.junit.Assume.assumeFalse;
public class BufferedFileImageInputStreamSpiTest extends ImageInputStreamSpiTest<File> {
@Override
protected ImageInputStreamSpi createProvider() {
return new BufferedFileImageInputStreamSpi();
}
@Override
protected File createInput() throws IOException {
return File.createTempFile("test-", ".tst");
}
@Test
public void testReturnNullWhenFileDoesNotExist() throws IOException {
// This is really stupid behavior, but it is consistent with the JRE bundled SPIs.
File input = new File("a-file-that-should-not-exist-ever.fnf");
assumeFalse("File should not exist: " + input.getPath(), input.exists());
assertNull(provider.createInputStreamInstance(input));
}
}

View File

@@ -0,0 +1,386 @@
/*
* 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.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: BufferedFileImageInputStreamTestCase.java,v 1.0 Apr 21, 2009 10:58:48 AM haraldk Exp$
*/
public class BufferedFileImageInputStreamTest {
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 {
BufferedFileImageInputStream stream = new BufferedFileImageInputStream(File.createTempFile("empty", ".tmp"));
assertEquals("Data length should be same as stream length", 0, stream.length());
}
@Test
public void testCreateNullFile() throws IOException {
try {
new BufferedFileImageInputStream((File) 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("file"));
assertTrue("Exception message does not contain null", message.contains("null"));
}
}
@Test
public void testCreateNullRAF() {
try {
new BufferedFileImageInputStream((RandomAccessFile) 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("raf"));
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);
BufferedFileImageInputStream stream = new BufferedFileImageInputStream(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);
BufferedFileImageInputStream stream = new BufferedFileImageInputStream(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);
BufferedFileImageInputStream stream = new BufferedFileImageInputStream(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);
BufferedFileImageInputStream stream = new BufferedFileImageInputStream(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 testReadBitRandom() throws IOException {
byte[] bytes = new byte[8];
File file = randomDataToFile(bytes);
long value = ByteBuffer.wrap(bytes).getLong();
// Create stream
ImageInputStream stream = new BufferedFileImageInputStream(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
ImageInputStream stream = new BufferedFileImageInputStream(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
ImageInputStream stream = new BufferedFileImageInputStream(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);
final ImageInputStream stream = new BufferedFileImageInputStream(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);
final ImageInputStream stream = new BufferedFileImageInputStream(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);
final ImageInputStream stream = new BufferedFileImageInputStream(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);
final ImageInputStream stream = new BufferedFileImageInputStream(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 testClose() throws IOException {
// Create wrapper stream
RandomAccessFile mock = mock(RandomAccessFile.class);
ImageInputStream stream = new BufferedFileImageInputStream(mock);
stream.close();
verify(mock, only()).close();
}
}

View File

@@ -32,16 +32,19 @@ package com.twelvemonkeys.imageio.stream;
import com.twelvemonkeys.io.ole2.CompoundDocument;
import com.twelvemonkeys.io.ole2.Entry;
import org.junit.Test;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Random;
import static java.util.Arrays.fill;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* BufferedImageInputStreamTest
@@ -72,6 +75,257 @@ public class BufferedImageInputStreamTest {
}
}
@Test
public void testReadBit() throws IOException {
byte[] bytes = new byte[] {(byte) 0xF0, (byte) 0x0F};
// Create wrapper stream
BufferedImageInputStream stream = new BufferedImageInputStream(new ByteArrayImageInputStream(bytes));
// Read all bits
assertEquals(1, stream.readBit());
assertEquals(1, stream.getBitOffset());
assertEquals(0, stream.getStreamPosition());
assertEquals(1, stream.readBit());
assertEquals(2, stream.getBitOffset());
assertEquals(0, stream.getStreamPosition());
assertEquals(1, stream.readBit());
assertEquals(3, stream.getBitOffset());
assertEquals(0, stream.getStreamPosition());
assertEquals(1, stream.readBit());
assertEquals(4, stream.getBitOffset());
assertEquals(0, stream.getStreamPosition());
assertEquals(0, stream.readBit());
assertEquals(5, stream.getBitOffset());
assertEquals(0, stream.getStreamPosition());
assertEquals(0, stream.readBit());
assertEquals(6, stream.getBitOffset());
assertEquals(0, stream.getStreamPosition());
assertEquals(0, stream.readBit());
assertEquals(7, stream.getBitOffset());
assertEquals(0, stream.getStreamPosition());
assertEquals(0, stream.readBit()); // last bit
assertEquals(0, stream.getBitOffset());
assertEquals(1, stream.getStreamPosition());
// Full reset, read same sequence again
stream.seek(0);
assertEquals(0, stream.getBitOffset());
assertEquals(0, stream.getStreamPosition());
assertEquals(1, stream.readBit());
assertEquals(1, stream.readBit());
assertEquals(1, stream.readBit());
assertEquals(1, stream.readBit());
assertEquals(0, stream.readBit());
assertEquals(0, stream.readBit());
assertEquals(0, stream.readBit());
assertEquals(0, stream.readBit());
assertEquals(0, stream.getBitOffset());
assertEquals(1, stream.getStreamPosition());
// Full reset, read partial
stream.seek(0);
assertEquals(1, stream.readBit());
assertEquals(1, stream.readBit());
// Byte reset, read same sequence again
stream.setBitOffset(0);
assertEquals(1, stream.readBit());
assertEquals(1, stream.readBit());
assertEquals(1, stream.readBit());
assertEquals(1, stream.readBit());
assertEquals(0, stream.readBit());
// Byte reset, read partial sequence again
stream.setBitOffset(3);
assertEquals(1, stream.readBit());
assertEquals(0, stream.readBit());
assertEquals(0, stream.getStreamPosition());
// Byte reset, read partial sequence again
stream.setBitOffset(6);
assertEquals(0, stream.readBit());
assertEquals(0, stream.readBit());
assertEquals(1, stream.getStreamPosition());
// Read all bits, second byte
assertEquals(0, stream.readBit());
assertEquals(1, stream.getBitOffset());
assertEquals(1, stream.getStreamPosition());
assertEquals(0, stream.readBit());
assertEquals(2, stream.getBitOffset());
assertEquals(1, stream.getStreamPosition());
assertEquals(0, stream.readBit());
assertEquals(3, stream.getBitOffset());
assertEquals(1, stream.getStreamPosition());
assertEquals(0, stream.readBit());
assertEquals(4, stream.getBitOffset());
assertEquals(1, stream.getStreamPosition());
assertEquals(1, stream.readBit());
assertEquals(5, stream.getBitOffset());
assertEquals(1, stream.getStreamPosition());
assertEquals(1, stream.readBit());
assertEquals(6, stream.getBitOffset());
assertEquals(1, stream.getStreamPosition());
assertEquals(1, stream.readBit());
assertEquals(7, stream.getBitOffset());
assertEquals(1, stream.getStreamPosition());
assertEquals(1, stream.readBit()); // last bit
assertEquals(0, stream.getBitOffset());
assertEquals(2, stream.getStreamPosition());
}
@Test
public void testReadBits() throws IOException {
byte[] bytes = new byte[] {(byte) 0xF0, (byte) 0xCC, (byte) 0xAA};
// Create wrapper stream
BufferedImageInputStream stream = new BufferedImageInputStream(new ByteArrayImageInputStream(bytes));
// Read all bits, first byte
assertEquals(3, stream.readBits(2));
assertEquals(2, stream.getBitOffset());
assertEquals(0, stream.getStreamPosition());
assertEquals(3, stream.readBits(2));
assertEquals(4, stream.getBitOffset());
assertEquals(0, stream.getStreamPosition());
assertEquals(0, stream.readBits(2));
assertEquals(6, stream.getBitOffset());
assertEquals(0, stream.getStreamPosition());
assertEquals(0, stream.readBits(2));
assertEquals(0, stream.getBitOffset());
assertEquals(1, stream.getStreamPosition());
// Read all bits, second byte
assertEquals(3, stream.readBits(2));
assertEquals(2, stream.getBitOffset());
assertEquals(1, stream.getStreamPosition());
assertEquals(0, stream.readBits(2));
assertEquals(4, stream.getBitOffset());
assertEquals(1, stream.getStreamPosition());
assertEquals(3, stream.readBits(2));
assertEquals(6, stream.getBitOffset());
assertEquals(1, stream.getStreamPosition());
assertEquals(0, stream.readBits(2));
assertEquals(0, stream.getBitOffset());
assertEquals(2, stream.getStreamPosition());
// Read all bits, third byte
assertEquals(2, stream.readBits(2));
assertEquals(2, stream.getBitOffset());
assertEquals(2, stream.getStreamPosition());
assertEquals(2, stream.readBits(2));
assertEquals(4, stream.getBitOffset());
assertEquals(2, stream.getStreamPosition());
assertEquals(2, stream.readBits(2));
assertEquals(6, stream.getBitOffset());
assertEquals(2, stream.getStreamPosition());
assertEquals(2, stream.readBits(2));
assertEquals(0, stream.getBitOffset());
assertEquals(3, stream.getStreamPosition());
// Full reset, read same sequence again
stream.seek(0);
assertEquals(0, stream.getBitOffset());
assertEquals(0, stream.getStreamPosition());
// Read all bits, increasing size
assertEquals(7, stream.readBits(3)); // 111
assertEquals(3, stream.getBitOffset());
assertEquals(0, stream.getStreamPosition());
assertEquals(8, stream.readBits(4)); // 1000
assertEquals(7, stream.getBitOffset());
assertEquals(0, stream.getStreamPosition());
assertEquals(12, stream.readBits(5)); // 01100
assertEquals(4, stream.getBitOffset());
assertEquals(1, stream.getStreamPosition());
assertEquals(50, stream.readBits(6)); // 110010
assertEquals(2, stream.getBitOffset());
assertEquals(2, stream.getStreamPosition());
assertEquals(42, stream.readBits(6)); // 101010
assertEquals(0, stream.getBitOffset());
assertEquals(3, stream.getStreamPosition());
// Full reset, read same sequence again
stream.seek(0);
assertEquals(0, stream.getBitOffset());
assertEquals(0, stream.getStreamPosition());
// Read all bits multi-byte
assertEquals(0xF0C, stream.readBits(12)); // 111100001100
assertEquals(4, stream.getBitOffset());
assertEquals(1, stream.getStreamPosition());
assertEquals(0xCAA, stream.readBits(12)); // 110010101010
assertEquals(0, stream.getBitOffset());
assertEquals(3, stream.getStreamPosition());
// Full reset, read same sequence again, all bits in one go
stream.seek(0);
assertEquals(0, stream.getBitOffset());
assertEquals(0, stream.getStreamPosition());
assertEquals(0xF0CCAA, stream.readBits(24));
}
@Test
public void testReadBitsRandom() throws IOException {
long value = random.nextLong();
byte[] bytes = new byte[8];
ByteBuffer.wrap(bytes).putLong(value);
// Create wrapper stream
BufferedImageInputStream stream = new BufferedImageInputStream(new ByteArrayImageInputStream(bytes));
for (int i = 1; i < 64; i++) {
stream.seek(0);
assertEquals(i + " bits differ", value >>> (64L - i), stream.readBits(i));
}
}
@Test
public void testClose() throws IOException {
// Create wrapper stream
ImageInputStream mock = mock(ImageInputStream.class);
BufferedImageInputStream stream = new BufferedImageInputStream(mock);
stream.close();
verify(mock, never()).close();
}
// TODO: Write other tests
// TODO: Create test that exposes read += -1 (eof) bug

View File

@@ -0,0 +1,18 @@
package com.twelvemonkeys.imageio.stream;
import javax.imageio.spi.ImageInputStreamSpi;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
public class BufferedRAFImageInputStreamSpiTest extends ImageInputStreamSpiTest<RandomAccessFile> {
@Override
protected ImageInputStreamSpi createProvider() {
return new BufferedRAFImageInputStreamSpi();
}
@Override
protected RandomAccessFile createInput() throws IOException {
return new RandomAccessFile(File.createTempFile("test-", ".tst"), "r");
}
}

View File

@@ -39,11 +39,11 @@ import static com.twelvemonkeys.imageio.stream.BufferedImageInputStreamTest.rang
import static org.junit.Assert.*;
/**
* ByteArrayImageInputStreamTestCase
* ByteArrayImageInputStreamTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: ByteArrayImageInputStreamTestCase.java,v 1.0 Apr 21, 2009 10:58:48 AM haraldk Exp$
* @version $Id: ByteArrayImageInputStreamTest.java,v 1.0 Apr 21, 2009 10:58:48 AM haraldk Exp$
*/
public class ByteArrayImageInputStreamTest {
private final Random random = new Random(1709843507234566L);

View File

@@ -11,14 +11,14 @@ import java.util.Locale;
import static org.junit.Assert.*;
abstract class ImageInputStreamSpiTest<T> {
private final ImageInputStreamSpi provider = createProvider();
protected final ImageInputStreamSpi provider = createProvider();
@SuppressWarnings("unchecked")
private final Class<T> inputClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
protected final Class<T> inputClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
protected abstract ImageInputStreamSpi createProvider();
protected abstract T createInput();
protected abstract T createInput() throws IOException;
@Test
public void testInputClass() {

View File

@@ -283,26 +283,6 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
reader.dispose();
}
@Test
public void testReadNoInput() throws IOException {
ImageReader reader = createReader();
// Do not set input
BufferedImage image = null;
try {
image = reader.read(0);
fail("Read image with no input");
}
catch (IllegalStateException ignore) {
}
catch (IOException e) {
failBecause("Image could not be read", e);
}
assertNull(image);
reader.dispose();
}
@Test
public void testReRead() throws IOException {
ImageReader reader = createReader();
@@ -323,69 +303,71 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
reader.dispose();
}
@Test
@Test(expected = IllegalStateException.class)
public void testReadNoInput() throws IOException {
ImageReader reader = createReader();
// Do not set input
try {
reader.read(0);
fail("Read image with no input");
}
catch (IOException e) {
failBecause("Image could not be read", e);
}
}
@Test(expected = IndexOutOfBoundsException.class)
public void testReadIndexNegativeWithParam() throws IOException {
ImageReader reader = createReader();
TestData data = getTestData().get(0);
reader.setInput(data.getInputStream());
BufferedImage image = null;
try {
image = reader.read(-1, reader.getDefaultReadParam());
reader.read(-1, reader.getDefaultReadParam());
fail("Read image with illegal index");
}
catch (IndexOutOfBoundsException ignore) {
}
catch (IOException e) {
failBecause("Image could not be read", e);
}
assertNull(image);
reader.dispose();
finally {
reader.dispose();
}
}
@Test
@Test(expected = IndexOutOfBoundsException.class)
public void testReadIndexOutOfBoundsWithParam() throws IOException {
ImageReader reader = createReader();
TestData data = getTestData().get(0);
reader.setInput(data.getInputStream());
BufferedImage image = null;
try {
image = reader.read(Short.MAX_VALUE, reader.getDefaultReadParam());
reader.read(Short.MAX_VALUE, reader.getDefaultReadParam());
fail("Read image with index out of bounds");
}
catch (IndexOutOfBoundsException ignore) {
}
catch (IOException e) {
failBecause("Image could not be read", e);
}
assertNull(image);
reader.dispose();
finally {
reader.dispose();
}
}
@Test
@Test(expected = IllegalStateException.class)
public void testReadNoInputWithParam() throws IOException {
ImageReader reader = createReader();
// Do not set input
BufferedImage image = null;
try {
image = reader.read(0, reader.getDefaultReadParam());
reader.read(0, reader.getDefaultReadParam());
fail("Read image with no input");
}
catch (IllegalStateException ignore) {
}
catch (IOException e) {
failBecause("Image could not be read", e);
}
assertNull(image);
reader.dispose();
finally {
reader.dispose();
}
}
@Test
@@ -553,10 +535,10 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
int actualRGB = actual.getRGB(x, y);
try {
assertEquals(String.format("%s alpha at (%d, %d)", message, x, y), (expectedRGB >>> 24) & 0xff, (actualRGB >>> 24) & 0xff, 5);
assertEquals(String.format("%s red at (%d, %d)", message, x, y), (expectedRGB >> 16) & 0xff, (actualRGB >> 16) & 0xff, 5);
assertEquals(String.format("%s green at (%d, %d)", message, x, y), (expectedRGB >> 8) & 0xff, (actualRGB >> 8) & 0xff, 5);
assertEquals(String.format("%s blue at (%d, %d)", message, x, y), expectedRGB & 0xff, actualRGB & 0xff, 5);
assertEquals((expectedRGB >>> 24) & 0xff, (actualRGB >>> 24) & 0xff, 5);
assertEquals((expectedRGB >> 16) & 0xff, (actualRGB >> 16) & 0xff, 5);
assertEquals((expectedRGB >> 8) & 0xff, (actualRGB >> 8) & 0xff, 5);
assertEquals(expectedRGB & 0xff, actualRGB & 0xff, 5);
}
catch (AssertionError e) {
File tempExpected = File.createTempFile("junit-expected-", ".png");
@@ -566,7 +548,6 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
System.err.println("tempActual.getAbsolutePath(): " + tempActual.getAbsolutePath());
ImageIO.write(actual, "PNG", tempActual);
assertEquals(String.format("%s ARGB at (%d, %d)", message, x, y), String.format("#%08x", expectedRGB), String.format("#%08x", actualRGB));
}
}
@@ -1650,12 +1631,72 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
BufferedImage one = reader.read(0);
BufferedImage two = reader.read(0);
// Test for same BufferedImage instance
assertNotSame("Multiple reads return same (mutable) image", one, two);
one.setRGB(0, 0, Color.BLUE.getRGB());
two.setRGB(0, 0, Color.RED.getRGB());
// Test for same backing storage (array)
one.setRGB(0, 0, Color.BLACK.getRGB());
two.setRGB(0, 0, Color.WHITE.getRGB());
assertTrue(one.getRGB(0, 0) != two.getRGB(0, 0));
reader.dispose();
}
@Test
public void testReadThumbnails() throws IOException {
T reader = createReader();
if (reader.readerSupportsThumbnails()) {
for (TestData testData : getTestData()) {
try (ImageInputStream inputStream = testData.getInputStream()) {
reader.setInput(inputStream);
int numImages = reader.getNumImages(true);
for (int i = 0; i < numImages; i++) {
int numThumbnails = reader.getNumThumbnails(0);
for (int t = 0; t < numThumbnails; t++) {
BufferedImage thumbnail = reader.readThumbnail(0, t);
assertNotNull(thumbnail);
}
}
}
}
}
reader.dispose();
}
@Test
public void testThumbnailProgress() throws IOException {
T reader = createReader();
IIOReadProgressListener listener = mock(IIOReadProgressListener.class);
reader.addIIOReadProgressListener(listener);
if (reader.readerSupportsThumbnails()) {
for (TestData testData : getTestData()) {
try (ImageInputStream inputStream = testData.getInputStream()) {
reader.setInput(inputStream);
int numThumbnails = reader.getNumThumbnails(0);
for (int i = 0; i < numThumbnails; i++) {
reset(listener);
reader.readThumbnail(0, i);
InOrder order = inOrder(listener);
order.verify(listener).thumbnailStarted(reader, 0, i);
order.verify(listener, atLeastOnce()).thumbnailProgress(reader, 100f);
order.verify(listener).thumbnailComplete(reader);
}
}
}
}
reader.dispose();
}

View File

@@ -30,6 +30,17 @@
package com.twelvemonkeys.imageio.plugins.iff;
import com.twelvemonkeys.image.ResampleOp;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder;
import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
@@ -41,23 +52,6 @@ import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import com.twelvemonkeys.image.ResampleOp;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.stream.BufferedImageInputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder;
/**
* Reader for Commodore Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) and PBM
* format (Packed BitMap).
@@ -824,8 +818,7 @@ public final class IFFImageReader extends ImageReaderBase {
continue;
}
try {
ImageInputStream input = new BufferedImageInputStream(ImageIO.createImageInputStream(file));
try (ImageInputStream input = ImageIO.createImageInputStream(file)) {
boolean canRead = reader.getOriginatingProvider().canDecodeInput(input);
if (canRead) {

View File

@@ -38,7 +38,7 @@ import java.io.IOException;
import java.io.InputStream;
/**
* Application.
* An application (APPn) segment in the JPEG stream.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
@@ -78,7 +78,9 @@ class Application extends Segment {
if ("JFXX".equals(identifier)) {
return JFXX.read(data, length);
}
// TODO: Exif?
if ("Exif".equals(identifier)) {
return EXIF.read(data, length);
}
case JPEG.APP2:
// ICC_PROFILE
if ("ICC_PROFILE".equals(identifier)) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, Harald Kuhr
* Copyright (c) 2021, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -30,41 +30,45 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import java.awt.image.BufferedImage;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import javax.imageio.stream.ImageInputStream;
import java.io.DataInput;
import java.io.EOFException;
import java.io.IOException;
/**
* JFIFThumbnailReader
* An EXIF segment.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: JFIFThumbnailReader.java,v 1.0 18.04.12 12:19 haraldk Exp$
* @version $Id: JFIFSegment.java,v 1.0 23.04.12 16:52 haraldk Exp$
*/
final class JFIFThumbnailReader extends ThumbnailReader {
private final JFIF segment;
JFIFThumbnailReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex, final JFIF segment) {
super(progressListener, imageIndex, thumbnailIndex);
this.segment = segment;
final class EXIF extends Application {
EXIF(byte[] data) {
super(JPEG.APP1, "Exif", data);
}
@Override
public BufferedImage read() {
processThumbnailStarted();
BufferedImage thumbnail = readRawThumbnail(segment.thumbnail, segment.thumbnail.length, 0, segment.xThumbnail, segment.yThumbnail);
processThumbnailProgress(100f);
processThumbnailComplete();
return thumbnail;
public String toString() {
return String.format("APP1/Exif, length: %d", data.length);
}
@Override
public int getWidth() throws IOException {
return segment.xThumbnail;
ImageInputStream exifData() {
// Identifier is "Exif\0" + 1 byte pad
int offset = identifier.length() + 2;
return new ByteArrayImageInputStream(data, offset, data.length - offset);
}
@Override
public int getHeight() throws IOException {
return segment.yThumbnail;
public static EXIF read(final DataInput data, int length) throws IOException {
if (length < 2 + 6) {
throw new EOFException();
}
byte[] bytes = new byte[length - 2];
data.readFully(bytes);
return new EXIF(bytes);
}
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright (c) 2012, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name 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.plugins.jpeg;
import com.twelvemonkeys.imageio.color.YCbCrConverter;
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
import com.twelvemonkeys.imageio.plugins.jpeg.ThumbnailReader.JPEGThumbnailReader;
import com.twelvemonkeys.imageio.plugins.jpeg.ThumbnailReader.UncompressedThumbnailReader;
import javax.imageio.IIOException;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.Arrays;
/**
* EXIFThumbnail
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: EXIFThumbnail.java,v 1.0 18.04.12 12:19 haraldk Exp$
*/
final class EXIFThumbnail {
private EXIFThumbnail() {
}
static ThumbnailReader from(final EXIF segment, final CompoundDirectory exif, final ImageReader jpegThumbnailReader) throws IOException {
if (segment != null && exif != null && exif.directoryCount() >= 2) {
ImageInputStream stream = segment.exifData(); // NOTE This is an in-memory stream and must not be closed...
Directory ifd1 = exif.getDirectory(1);
// Compression: 1 = no compression, 6 = JPEG compression (default)
Entry compressionEntry = ifd1.getEntryById(TIFF.TAG_COMPRESSION);
int compression = compressionEntry == null ? 6 : ((Number) compressionEntry.getValue()).intValue();
switch (compression) {
case 1:
return createUncompressedThumbnailReader(stream, ifd1);
case 6:
return createJPEGThumbnailReader(segment, jpegThumbnailReader, stream, ifd1);
default:
throw new IIOException("EXIF IFD with unknown thumbnail compression (expected 1 or 6): " + compression);
}
}
return null;
}
private static UncompressedThumbnailReader createUncompressedThumbnailReader(ImageInputStream stream, Directory ifd1) throws IOException {
Entry stripOffEntry = ifd1.getEntryById(TIFF.TAG_STRIP_OFFSETS);
Entry width = ifd1.getEntryById(TIFF.TAG_IMAGE_WIDTH);
Entry height = ifd1.getEntryById(TIFF.TAG_IMAGE_HEIGHT);
if (stripOffEntry != null && width != null && height != null) {
Entry bitsPerSample = ifd1.getEntryById(TIFF.TAG_BITS_PER_SAMPLE);
Entry samplesPerPixel = ifd1.getEntryById(TIFF.TAG_SAMPLES_PER_PIXEL);
Entry photometricInterpretation = ifd1.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION);
// Required
int w = ((Number) width.getValue()).intValue();
int h = ((Number) height.getValue()).intValue();
if (bitsPerSample != null && !Arrays.equals((int[]) bitsPerSample.getValue(), new int[] {8, 8, 8})) {
throw new IIOException("Unknown BitsPerSample value for uncompressed EXIF thumbnail (expected [8, 8, 8]): " + bitsPerSample.getValueAsString());
}
if (samplesPerPixel != null && ((Number) samplesPerPixel.getValue()).intValue() != 3) {
throw new IIOException("Unknown SamplesPerPixel value for uncompressed EXIF thumbnail (expected 3): " + samplesPerPixel.getValueAsString());
}
int interpretation = photometricInterpretation != null ? ((Number) photometricInterpretation.getValue()).intValue() : 2;
long stripOffset = ((Number) stripOffEntry.getValue()).longValue();
int thumbLength = w * h * 3;
if (stripOffset >= 0 && stripOffset + thumbLength <= stream.length()) {
// Read raw image data, either RGB or YCbCr
stream.seek(stripOffset);
byte[] thumbData = new byte[thumbLength];
stream.readFully(thumbData);
switch (interpretation) {
case 2:
// RGB
break;
case 6:
// YCbCr
for (int i = 0; i < thumbLength; i += 3) {
YCbCrConverter.convertYCbCr2RGB(thumbData, thumbData, i);
}
break;
default:
throw new IIOException("Unknown PhotometricInterpretation value for uncompressed EXIF thumbnail (expected 2 or 6): " + interpretation);
}
return new UncompressedThumbnailReader(w, h, thumbData);
}
}
throw new IIOException("EXIF IFD with empty or incomplete uncompressed thumbnail");
}
private static JPEGThumbnailReader createJPEGThumbnailReader(EXIF exif, ImageReader jpegThumbnailReader, ImageInputStream stream, Directory ifd1) throws IOException {
Entry jpegOffEntry = ifd1.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT);
if (jpegOffEntry != null) {
Entry jpegLenEntry = ifd1.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
// Test if Exif thumbnail is contained within the Exif segment (offset + length <= segment.length)
long jpegOffset = ((Number) jpegOffEntry.getValue()).longValue();
long jpegLength = jpegLenEntry != null ? ((Number) jpegLenEntry.getValue()).longValue() : -1;
if (jpegLength > 0 && jpegOffset + jpegLength <= exif.data.length) {
// Verify first bytes are FFD8
stream.seek(jpegOffset);
stream.setByteOrder(ByteOrder.BIG_ENDIAN);
if (stream.readUnsignedShort() == JPEG.SOI) {
return new JPEGThumbnailReader(jpegThumbnailReader, stream, jpegOffset);
}
}
}
throw new IIOException("EXIF IFD with empty or incomplete JPEG thumbnail");
}
}

View File

@@ -1,248 +0,0 @@
/*
* Copyright (c) 2012, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name 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.plugins.jpeg;
import com.twelvemonkeys.imageio.color.YCbCrConverter;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.lang.Validate;
import javax.imageio.IIOException;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.lang.ref.SoftReference;
import java.util.Arrays;
/**
* EXIFThumbnail
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: EXIFThumbnail.java,v 1.0 18.04.12 12:19 haraldk Exp$
*/
final class EXIFThumbnailReader extends ThumbnailReader {
private final ImageReader reader;
private final Directory ifd;
private final ImageInputStream stream;
private final int compression;
private transient SoftReference<BufferedImage> cachedThumbnail;
EXIFThumbnailReader(final ThumbnailReadProgressListener progressListener, final ImageReader jpegReader, final int imageIndex, final int thumbnailIndex, final Directory ifd, final ImageInputStream stream) {
super(progressListener, imageIndex, thumbnailIndex);
this.reader = Validate.notNull(jpegReader);
this.ifd = ifd;
this.stream = stream;
Entry compression = ifd.getEntryById(TIFF.TAG_COMPRESSION);
this.compression = compression != null ? ((Number) compression.getValue()).intValue() : 6;
}
@Override
public BufferedImage read() throws IOException {
if (compression == 1) { // 1 = no compression
processThumbnailStarted();
BufferedImage thumbnail = readUncompressed();
processThumbnailProgress(100f);
processThumbnailComplete();
return thumbnail;
}
else if (compression == 6) { // 6 = JPEG compression
processThumbnailStarted();
BufferedImage thumbnail = readJPEGCached(true);
processThumbnailProgress(100f);
processThumbnailComplete();
return thumbnail;
}
else {
throw new IIOException("Unsupported EXIF thumbnail compression: " + compression);
}
}
private BufferedImage readJPEGCached(final boolean pixelsExposed) throws IOException {
BufferedImage thumbnail = cachedThumbnail != null ? cachedThumbnail.get() : null;
if (thumbnail == null) {
thumbnail = readJPEG();
}
cachedThumbnail = pixelsExposed ? null : new SoftReference<>(thumbnail);
return thumbnail;
}
private BufferedImage readJPEG() throws IOException {
// IFD1 should contain JPEG offset for JPEG thumbnail
Entry jpegOffset = ifd.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT);
if (jpegOffset != null) {
stream.seek(((Number) jpegOffset.getValue()).longValue());
InputStream input = IIOUtil.createStreamAdapter(stream);
// For certain EXIF files (encoded with TIFF.TAG_YCBCR_POSITIONING = 2?), we need
// EXIF information to read the thumbnail correctly (otherwise the colors are messed up).
// Probably related to: http://bugs.sun.com/view_bug.do?bug_id=4881314
// HACK: Splice empty EXIF information into the thumbnail stream
byte[] fakeEmptyExif = {
// SOI (from original data)
(byte) input.read(), (byte) input.read(),
// APP1 + len (016) + 'Exif' + 0-term + pad
(byte) 0xFF, (byte) 0xE1, 0, 16, 'E', 'x', 'i', 'f', 0, 0,
// Big-endian BOM (MM), TIFF magic (042), offset (0000)
'M', 'M', 0, 42, 0, 0, 0, 0,
};
input = new SequenceInputStream(new ByteArrayInputStream(fakeEmptyExif), input);
try {
try (MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(input)) {
return readJPEGThumbnail(reader, stream);
}
}
finally {
input.close();
}
}
throw new IIOException("Missing JPEGInterchangeFormat tag for JPEG compressed EXIF thumbnail");
}
private BufferedImage readUncompressed() throws IOException {
// Read ImageWidth, ImageLength (height) and BitsPerSample (=8 8 8, always)
// PhotometricInterpretation (2=RGB, 6=YCbCr), SamplesPerPixel (=3, always),
Entry width = ifd.getEntryById(TIFF.TAG_IMAGE_WIDTH);
Entry height = ifd.getEntryById(TIFF.TAG_IMAGE_HEIGHT);
if (width == null || height == null) {
throw new IIOException("Missing dimensions for uncompressed EXIF thumbnail");
}
Entry bitsPerSample = ifd.getEntryById(TIFF.TAG_BITS_PER_SAMPLE);
Entry samplesPerPixel = ifd.getEntryById(TIFF.TAG_SAMPLES_PER_PIXEL);
Entry photometricInterpretation = ifd.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION);
// Required
int w = ((Number) width.getValue()).intValue();
int h = ((Number) height.getValue()).intValue();
if (bitsPerSample != null) {
int[] bpp = (int[]) bitsPerSample.getValue();
if (!Arrays.equals(bpp, new int[] {8, 8, 8})) {
throw new IIOException("Unknown BitsPerSample value for uncompressed EXIF thumbnail (expected [8, 8, 8]): " + bitsPerSample.getValueAsString());
}
}
if (samplesPerPixel != null && (Integer) samplesPerPixel.getValue() != 3) {
throw new IIOException("Unknown SamplesPerPixel value for uncompressed EXIF thumbnail (expected 3): " + samplesPerPixel.getValueAsString());
}
int interpretation = photometricInterpretation != null ? ((Number) photometricInterpretation.getValue()).intValue() : 2;
// IFD1 should contain strip offsets for uncompressed images
Entry offset = ifd.getEntryById(TIFF.TAG_STRIP_OFFSETS);
if (offset != null) {
stream.seek(((Number) offset.getValue()).longValue());
// Read raw image data, either RGB or YCbCr
int thumbSize = w * h * 3;
byte[] thumbData = JPEGImageReader.readFully(stream, thumbSize);
switch (interpretation) {
case 2:
// RGB
break;
case 6:
// YCbCr
for (int i = 0; i < thumbSize; i += 3) {
YCbCrConverter.convertYCbCr2RGB(thumbData, thumbData, i);
}
break;
default:
throw new IIOException("Unknown PhotometricInterpretation value for uncompressed EXIF thumbnail (expected 2 or 6): " + interpretation);
}
return ThumbnailReader.readRawThumbnail(thumbData, thumbSize, 0, w, h);
}
throw new IIOException("Missing StripOffsets tag for uncompressed EXIF thumbnail");
}
@Override
public int getWidth() throws IOException {
if (compression == 1) { // 1 = no compression
Entry width = ifd.getEntryById(TIFF.TAG_IMAGE_WIDTH);
if (width == null) {
throw new IIOException("Missing dimensions for uncompressed EXIF thumbnail");
}
return ((Number) width.getValue()).intValue();
}
else if (compression == 6) { // 6 = JPEG compression
return readJPEGCached(false).getWidth();
}
else {
throw new IIOException("Unsupported EXIF thumbnail compression (expected 1 or 6): " + compression);
}
}
@Override
public int getHeight() throws IOException {
if (compression == 1) { // 1 = no compression
Entry height = ifd.getEntryById(TIFF.TAG_IMAGE_HEIGHT);
if (height == null) {
throw new IIOException("Missing dimensions for uncompressed EXIF thumbnail");
}
return ((Number) height.getValue()).intValue();
}
else if (compression == 6) { // 6 = JPEG compression
return readJPEGCached(false).getHeight();
}
else {
throw new IIOException("Unsupported EXIF thumbnail compression (expected 1 or 6): " + compression);
}
}
}

View File

@@ -38,7 +38,7 @@ import java.io.IOException;
import java.nio.ByteBuffer;
/**
* JFIFSegment
* A JFIF segment.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
@@ -54,8 +54,8 @@ final class JFIF extends Application {
final int yThumbnail;
final byte[] thumbnail;
private JFIF(int majorVersion, int minorVersion, int units, int xDensity, int yDensity, int xThumbnail, int yThumbnail, byte[] thumbnail, byte[] data) {
super(JPEG.APP0, "JFIF", data);
JFIF(int majorVersion, int minorVersion, int units, int xDensity, int yDensity, int xThumbnail, int yThumbnail, byte[] thumbnail) {
super(JPEG.APP0, "JFIF", new byte[5 + 9 + (thumbnail != null ? thumbnail.length : 0)]);
this.majorVersion = majorVersion;
this.minorVersion = minorVersion;
@@ -98,7 +98,7 @@ final class JFIF extends Application {
throw new EOFException();
}
data.readFully(new byte[5]);
data.readFully(new byte[5]); // Skip "JFIF\0"
byte[] bytes = new byte[length - 2 - 5];
data.readFully(bytes);
@@ -115,8 +115,7 @@ final class JFIF extends Application {
buffer.getShort() & 0xffff,
x = buffer.get() & 0xff,
y = buffer.get() & 0xff,
getBytes(buffer, Math.min(buffer.remaining(), x * y * 3)),
bytes
getBytes(buffer, Math.min(buffer.remaining(), x * y * 3))
);
}

View File

@@ -30,17 +30,31 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.plugins.jpeg.ThumbnailReader.UncompressedThumbnailReader;
import javax.imageio.IIOException;
import java.io.IOException;
/**
* ThumbnailReadProgressListener
* JFIFThumbnail
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: ThumbnailReadProgressListener.java,v 1.0 07.05.12 10:15 haraldk Exp$
* @version $Id: JFIFThumbnail.java,v 1.0 18.04.12 12:19 haraldk Exp$
*/
interface ThumbnailReadProgressListener {
void thumbnailStarted(int imageIndex, int thumbnailIndex);
final class JFIFThumbnail {
private JFIFThumbnail() {
}
void thumbnailProgress(float percentageDone);
static ThumbnailReader from(final JFIF segment) throws IOException {
if (segment != null && segment.xThumbnail > 0 && segment.yThumbnail > 0) {
if (segment.thumbnail == null || segment.thumbnail.length < segment.xThumbnail * segment.yThumbnail) {
throw new IIOException("Truncated JFIF thumbnail");
}
void thumbnailComplete();
return new UncompressedThumbnailReader(segment.xThumbnail, segment.yThumbnail, segment.thumbnail);
}
return null;
}
}

View File

@@ -35,7 +35,7 @@ import java.io.IOException;
import java.util.Arrays;
/**
* JFXXSegment
* A JFXX segment (aka JFIF extension segment).
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
@@ -49,8 +49,8 @@ final class JFXX extends Application {
final int extensionCode;
final byte[] thumbnail;
private JFXX(final int extensionCode, final byte[] thumbnail, final byte[] data) {
super(com.twelvemonkeys.imageio.metadata.jpeg.JPEG.APP0, "JFXX", data);
JFXX(final int extensionCode, final byte[] thumbnail) {
super(com.twelvemonkeys.imageio.metadata.jpeg.JPEG.APP0, "JFXX", new byte[1 + (thumbnail != null ? thumbnail.length : 0)]);
this.extensionCode = extensionCode;
this.thumbnail = thumbnail;
@@ -82,8 +82,7 @@ final class JFXX extends Application {
return new JFXX(
bytes[0] & 0xff,
bytes.length - 1 > 0 ? Arrays.copyOfRange(bytes, 1, bytes.length - 1) : null,
bytes
bytes.length - 1 > 0 ? Arrays.copyOfRange(bytes, 1, bytes.length - 1) : null
);
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright (c) 2012, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name 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.plugins.jpeg;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import com.twelvemonkeys.imageio.plugins.jpeg.ThumbnailReader.IndexedThumbnailReader;
import com.twelvemonkeys.imageio.plugins.jpeg.ThumbnailReader.JPEGThumbnailReader;
import com.twelvemonkeys.imageio.plugins.jpeg.ThumbnailReader.UncompressedThumbnailReader;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import javax.imageio.IIOException;
import javax.imageio.ImageReader;
import java.io.IOException;
/**
* JFXXThumbnailReader
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: JFXXThumbnailReader.java,v 1.0 18.04.12 12:19 haraldk Exp$
*/
final class JFXXThumbnail {
private JFXXThumbnail() {
}
static ThumbnailReader from(final JFXX segment, final ImageReader thumbnailReader) throws IOException {
if (segment != null) {
if (segment.thumbnail != null && segment.thumbnail.length > 2) {
switch (segment.extensionCode) {
case JFXX.JPEG:
if (((segment.thumbnail[0] & 0xff) << 8 | segment.thumbnail[1] & 0xff) == JPEG.SOI) {
return new JPEGThumbnailReader(thumbnailReader, new ByteArrayImageInputStream(segment.thumbnail), 0);
}
break;
case JFXX.INDEXED:
int w = segment.thumbnail[0] & 0xff;
int h = segment.thumbnail[1] & 0xff;
if (segment.thumbnail.length >= 2 + 768 + w * h) {
return new IndexedThumbnailReader(w, h, segment.thumbnail, 2, segment.thumbnail, 2 + 768);
}
break;
case JFXX.RGB:
w = segment.thumbnail[0] & 0xff;
h = segment.thumbnail[1] & 0xff;
if (segment.thumbnail.length >= 2 + w * h * 3) {
return new UncompressedThumbnailReader(w, h, segment.thumbnail, 2);
}
break;
default:
throw new IIOException(String.format("Unknown JFXX extension code: %d, ignoring thumbnail", segment.extensionCode));
}
}
throw new IIOException("JFXX segment truncated, ignoring thumbnail");
}
return null;
}
}

View File

@@ -1,178 +0,0 @@
/*
* Copyright (c) 2012, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name 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.plugins.jpeg;
import com.twelvemonkeys.image.InverseColorMapIndexColorModel;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.lang.Validate;
import javax.imageio.IIOException;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.*;
import java.io.IOException;
import java.lang.ref.SoftReference;
/**
* JFXXThumbnailReader
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: JFXXThumbnailReader.java,v 1.0 18.04.12 12:19 haraldk Exp$
*/
final class JFXXThumbnailReader extends ThumbnailReader {
private final ImageReader reader;
private final JFXX segment;
private transient SoftReference<BufferedImage> cachedThumbnail;
JFXXThumbnailReader(final ThumbnailReadProgressListener progressListener, final ImageReader jpegReader, final int imageIndex, final int thumbnailIndex, final JFXX segment) {
super(progressListener, imageIndex, thumbnailIndex);
this.reader = Validate.notNull(jpegReader);
this.segment = segment;
}
@Override
public BufferedImage read() throws IOException {
processThumbnailStarted();
BufferedImage thumbnail;
switch (segment.extensionCode) {
case JFXX.JPEG:
thumbnail = readJPEGCached(true);
break;
case JFXX.INDEXED:
thumbnail = readIndexed();
break;
case JFXX.RGB:
thumbnail = readRGB();
break;
default:
throw new IIOException(String.format("Unsupported JFXX extension code: %d", segment.extensionCode));
}
processThumbnailProgress(100f);
processThumbnailComplete();
return thumbnail;
}
IIOMetadata readMetadata() throws IOException {
ImageInputStream input = new ByteArrayImageInputStream(segment.thumbnail);
try {
reader.setInput(input);
return reader.getImageMetadata(0);
}
finally {
input.close();
}
}
private BufferedImage readJPEGCached(boolean pixelsExposed) throws IOException {
BufferedImage thumbnail = cachedThumbnail != null ? cachedThumbnail.get() : null;
if (thumbnail == null) {
ImageInputStream stream = new ByteArrayImageInputStream(segment.thumbnail);
try {
thumbnail = readJPEGThumbnail(reader, stream);
}
finally {
stream.close();
}
}
cachedThumbnail = pixelsExposed ? null : new SoftReference<>(thumbnail);
return thumbnail;
}
@Override
public int getWidth() throws IOException {
switch (segment.extensionCode) {
case JFXX.RGB:
case JFXX.INDEXED:
return segment.thumbnail[0] & 0xff;
case JFXX.JPEG:
return readJPEGCached(false).getWidth();
default:
throw new IIOException(String.format("Unsupported JFXX extension code: %d", segment.extensionCode));
}
}
@Override
public int getHeight() throws IOException {
switch (segment.extensionCode) {
case JFXX.RGB:
case JFXX.INDEXED:
return segment.thumbnail[1] & 0xff;
case JFXX.JPEG:
return readJPEGCached(false).getHeight();
default:
throw new IIOException(String.format("Unsupported JFXX extension code: %d", segment.extensionCode));
}
}
private BufferedImage readIndexed() {
// 1 byte: xThumb
// 1 byte: yThumb
// 768 bytes: palette
// x * y bytes: 8 bit indexed pixels
int w = segment.thumbnail[0] & 0xff;
int h = segment.thumbnail[1] & 0xff;
int[] rgbs = new int[256];
for (int i = 0; i < rgbs.length; i++) {
rgbs[i] = (segment.thumbnail[3 * i + 2] & 0xff) << 16
| (segment.thumbnail[3 * i + 3] & 0xff) << 8
| (segment.thumbnail[3 * i + 4] & 0xff);
}
IndexColorModel icm = new InverseColorMapIndexColorModel(8, rgbs.length, rgbs, 0, false, -1, DataBuffer.TYPE_BYTE);
DataBufferByte buffer = new DataBufferByte(segment.thumbnail, segment.thumbnail.length - 770, 770);
WritableRaster raster = Raster.createPackedRaster(buffer, w, h, 8, null);
return new BufferedImage(icm, raster, icm.isAlphaPremultiplied(), null);
}
private BufferedImage readRGB() {
// 1 byte: xThumb
// 1 byte: yThumb
// 3 * x * y bytes: 24 bit RGB pixels
int w = segment.thumbnail[0] & 0xff;
int h = segment.thumbnail[1] & 0xff;
return ThumbnailReader.readRawThumbnail(segment.thumbnail, segment.thumbnail.length - 2, 2, w, h);
}
}

View File

@@ -32,6 +32,7 @@ package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import com.twelvemonkeys.xml.XMLSerializer;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
@@ -132,7 +133,7 @@ final class JPEGImage10MetadataCleaner {
IIOMetadataNode app0JFXX = new IIOMetadataNode("app0JFXX");
app0JFXX.setAttribute("extensionCode", String.valueOf(jfxx.extensionCode));
JFXXThumbnailReader thumbnailReader = new JFXXThumbnailReader(null, reader.getThumbnailReader(), 0, 0, jfxx);
ThumbnailReader thumbnailReader = JFXXThumbnail.from(jfxx, reader.getThumbnailReader());
IIOMetadataNode jfifThumb;
switch (jfxx.extensionCode) {

View File

@@ -30,29 +30,21 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.*;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.color.ColorSpaces;
import com.twelvemonkeys.imageio.color.YCbCrConverter;
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.lang.Validate;
import com.twelvemonkeys.xml.XMLSerializer;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.*;
import javax.imageio.event.IIOReadUpdateListener;
import javax.imageio.event.IIOReadWarningListener;
import javax.imageio.metadata.IIOMetadata;
@@ -60,25 +52,14 @@ import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.color.ColorSpaces;
import com.twelvemonkeys.imageio.color.YCbCrConverter;
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
import com.twelvemonkeys.imageio.stream.BufferedImageInputStream;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.lang.Validate;
import com.twelvemonkeys.xml.XMLSerializer;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.*;
import java.io.*;
import java.util.List;
import java.util.*;
/**
* A JPEG {@code ImageReader} implementation based on the JRE {@code JPEGImageReader},
@@ -140,7 +121,7 @@ public final class JPEGImageReader extends ImageReaderBase {
private List<Segment> segments;
private int currentStreamIndex = 0;
private List<Long> streamOffsets = new ArrayList<>();
private final List<Long> streamOffsets = new ArrayList<>();
protected JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) {
super(provider);
@@ -192,19 +173,7 @@ public final class JPEGImageReader extends ImageReaderBase {
private boolean isLossless() throws IOException {
assertInput();
try {
if (getSOF().marker == JPEG.SOF3) {
return true;
}
}
catch (IIOException ignore) {
// May happen if no SOF is found, in case we'll just fall through
if (DEBUG) {
ignore.printStackTrace();
}
}
return false;
return getSOF().marker == JPEG.SOF3;
}
@Override
@@ -681,7 +650,7 @@ public final class JPEGImageReader extends ImageReaderBase {
private void initDelegate(boolean seekForwardOnly, boolean ignoreMetadata) throws IOException {
// JPEGSegmentImageInputStream that filters out/skips bad/unnecessary segments
delegate.setInput(imageInput != null
? new JPEGSegmentImageInputStream(new SubImageInputStream(imageInput, Long.MAX_VALUE), new JPEGSegmentStreamWarningDelegate())
? new JPEGSegmentImageInputStream(new SubImageInputStream(imageInput, Long.MAX_VALUE), new JPEGSegmentWarningDelegate())
: null, seekForwardOnly, ignoreMetadata);
}
@@ -719,6 +688,7 @@ public final class JPEGImageReader extends ImageReaderBase {
}
private void initHeader(final int imageIndex) throws IOException {
assertInput();
if (imageIndex < 0) {
throw new IndexOutOfBoundsException("imageIndex < 0: " + imageIndex);
}
@@ -747,26 +717,26 @@ public final class JPEGImageReader extends ImageReaderBase {
long lastKnownSOIOffset = streamOffsets.get(streamOffsets.size() - 1);
imageInput.seek(lastKnownSOIOffset);
try (ImageInputStream stream = new BufferedImageInputStream(imageInput)) { // Extreme (10s -> 50ms) speedup if imageInput is FileIIS
try {
for (int i = streamOffsets.size() - 1; i < imageIndex; i++) {
long start = 0;
if (DEBUG) {
start = System.currentTimeMillis();
System.out.println(String.format("Start seeking for image index %d", i + 1));
System.out.printf("Start seeking for image index %d%n", i + 1);
}
// Need to skip over segments, as they may contain JPEG markers (eg. JFXX or EXIF thumbnail)
JPEGSegmentUtil.readSegments(stream, Collections.<Integer, List<String>>emptyMap());
JPEGSegmentUtil.readSegments(imageInput, Collections.<Integer, List<String>>emptyMap());
// Now, search for EOI and following SOI...
int marker;
while ((marker = stream.read()) != -1) {
if (marker == 0xFF && (0xFF00 | stream.readUnsignedByte()) == JPEG.EOI) {
while ((marker = imageInput.read()) != -1) {
if (marker == 0xFF && (0xFF00 | imageInput.readUnsignedByte()) == JPEG.EOI) {
// Found EOI, now the SOI should be nearby...
while ((marker = stream.read()) != -1) {
if (marker == 0xFF && (0xFF00 | stream.readUnsignedByte()) == JPEG.SOI) {
long nextSOIOffset = stream.getStreamPosition() - 2;
while ((marker = imageInput.read()) != -1) {
if (marker == 0xFF && (0xFF00 | imageInput.readUnsignedByte()) == JPEG.SOI) {
long nextSOIOffset = imageInput.getStreamPosition() - 2;
imageInput.seek(nextSOIOffset);
streamOffsets.add(nextSOIOffset);
@@ -780,10 +750,9 @@ public final class JPEGImageReader extends ImageReaderBase {
}
if (DEBUG) {
System.out.println(String.format("Seek in %d ms", System.currentTimeMillis() - start));
System.out.printf("Seek in %d ms%n", System.currentTimeMillis() - start);
}
}
}
catch (EOFException eof) {
IndexOutOfBoundsException ioobe = new IndexOutOfBoundsException("Image index " + imageIndex + " not found in stream");
@@ -843,9 +812,9 @@ public final class JPEGImageReader extends ImageReaderBase {
return JPEGSegmentUtil.readSegments(imageInput, JPEGSegmentUtil.ALL_SEGMENTS);
}
catch (IIOException | IllegalArgumentException ignore) {
catch (IIOException | IllegalArgumentException e) {
if (DEBUG) {
ignore.printStackTrace();
e.printStackTrace();
}
}
finally {
@@ -904,38 +873,30 @@ public final class JPEGImageReader extends ImageReaderBase {
return jfxx.isEmpty() ? null : (JFXX) jfxx.get(0);
}
private CompoundDirectory getExif() throws IOException {
List<Application> exifSegments = getAppSegments(JPEG.APP1, "Exif");
private EXIF getExif() throws IOException {
List<Application> exif = getAppSegments(JPEG.APP1, "Exif");
return exif.isEmpty() ? null : (EXIF) exif.get(0); // TODO: Can there actually be more Exif segments?
}
if (!exifSegments.isEmpty()) {
Application exif = exifSegments.get(0);
int offset = exif.identifier.length() + 2; // Incl. pad
if (exif.data.length <= offset) {
processWarningOccurred("Exif chunk has no data.");
}
else {
// TODO: Consider returning ByteArrayImageInputStream from Segment.data()
try (ImageInputStream stream = new ByteArrayImageInputStream(exif.data, offset, exif.data.length - offset)) {
private CompoundDirectory parseExif(final EXIF exif) throws IOException {
if (exif != null) {
// Identifier is "Exif\0" + 1 byte pad
if (exif.data.length > exif.identifier.length() + 2) {
try (ImageInputStream stream = exif.exifData()) {
return (CompoundDirectory) new TIFFReader().read(stream);
}
catch (IIOException e) {
processWarningOccurred("Exif chunk is present, but can't be read: " + e.getMessage());
}
}
else {
processWarningOccurred("Exif chunk has no data.");
}
}
return null;
}
// TODO: Util method?
static byte[] readFully(DataInput stream, int len) throws IOException {
if (len == 0) {
return null;
}
byte[] data = new byte[len];
stream.readFully(data);
return data;
}
ICC_Profile getEmbeddedICCProfile(final boolean allowBadIndexes) throws IOException {
// ICC v 1.42 (2006) annex B:
// APP2 marker (0xFFE2) + 2 byte length + ASCII 'ICC_PROFILE' + 0 (termination)
@@ -1101,79 +1062,40 @@ public final class JPEGImageReader extends ImageReaderBase {
if (thumbnails == null) {
thumbnails = new ArrayList<>();
ThumbnailReadProgressListener thumbnailProgressDelegator = new ThumbnailProgressDelegate();
// Read JFIF thumbnails if present
JFIF jfif = getJFIF();
if (jfif != null && jfif.thumbnail != null) {
thumbnails.add(new JFIFThumbnailReader(thumbnailProgressDelegator, imageIndex, thumbnails.size(), jfif));
try {
ThumbnailReader thumbnail = JFIFThumbnail.from(getJFIF());
if (thumbnail != null) {
thumbnails.add(thumbnail);
}
}
catch (IOException e) {
processWarningOccurred(e.getMessage());
}
// Read JFXX thumbnails if present
JFXX jfxx = getJFXX();
if (jfxx != null && jfxx.thumbnail != null) {
switch (jfxx.extensionCode) {
case JFXX.JPEG:
case JFXX.INDEXED:
case JFXX.RGB:
thumbnails.add(new JFXXThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), imageIndex, thumbnails.size(), jfxx));
break;
default:
processWarningOccurred("Unknown JFXX extension code: " + jfxx.extensionCode);
try {
ThumbnailReader thumbnail = JFXXThumbnail.from(getJFXX(), getThumbnailReader());
if (thumbnail != null) {
thumbnails.add(thumbnail);
}
}
catch (IOException e) {
processWarningOccurred(e.getMessage());
}
// Read Exif thumbnails if present
List<Application> exifSegments = getAppSegments(JPEG.APP1, "Exif");
if (!exifSegments.isEmpty()) {
Application exif = exifSegments.get(0);
// Identifier is "Exif\0" + 1 byte pad
int offset = exif.identifier.length() + 2;
if (exif.data.length <= offset) {
processWarningOccurred("Exif chunk has no data.");
}
else {
ImageInputStream stream = new ByteArrayImageInputStream(exif.data, offset, exif.data.length - offset);
CompoundDirectory exifMetadata = (CompoundDirectory) new TIFFReader().read(stream);
if (exifMetadata.directoryCount() == 2) {
Directory ifd1 = exifMetadata.getDirectory(1);
// Compression: 1 = no compression, 6 = JPEG compression (default)
Entry compressionEntry = ifd1.getEntryById(TIFF.TAG_COMPRESSION);
int compression = compressionEntry == null ? 6 : ((Number) compressionEntry.getValue()).intValue();
if (compression == 6) {
if (ifd1.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT) != null) {
Entry jpegLength = ifd1.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
if ((jpegLength == null || ((Number) jpegLength.getValue()).longValue() > 0)) {
thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), 0, thumbnails.size(), ifd1, stream));
}
else {
processWarningOccurred("EXIF IFD with empty (zero-length) thumbnail");
}
}
else {
processWarningOccurred("EXIF IFD with JPEG thumbnail missing JPEGInterchangeFormat tag");
}
}
else if (compression == 1) {
if (ifd1.getEntryById(TIFF.TAG_STRIP_OFFSETS) != null) {
thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), 0, thumbnails.size(), ifd1, stream));
}
else {
processWarningOccurred("EXIF IFD with uncompressed thumbnail missing StripOffsets tag");
}
}
else {
processWarningOccurred("EXIF IFD with unknown compression (expected 1 or 6): " + compression);
}
}
try {
EXIF exif = getExif();
ThumbnailReader thumbnailReader = EXIFThumbnail.from(exif, parseExif(exif), getThumbnailReader());
if (thumbnailReader != null) {
thumbnails.add(thumbnailReader);
}
}
catch (IOException e) {
processWarningOccurred(e.getMessage());
}
}
}
@@ -1215,7 +1137,15 @@ public final class JPEGImageReader extends ImageReaderBase {
public BufferedImage readThumbnail(int imageIndex, int thumbnailIndex) throws IOException {
checkThumbnailBounds(imageIndex, thumbnailIndex);
return thumbnails.get(thumbnailIndex).read();
processThumbnailStarted(imageIndex, thumbnailIndex);
processThumbnailProgress(0f);
BufferedImage thumbnail = thumbnails.get(thumbnailIndex).read();;
processThumbnailProgress(100f);
processThumbnailComplete();
return thumbnail;
}
// Metadata
@@ -1224,7 +1154,7 @@ public final class JPEGImageReader extends ImageReaderBase {
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
initHeader(imageIndex);
return new JPEGImage10Metadata(segments, getSOF(), getJFIF(), getJFXX(), getEmbeddedICCProfile(true), getAdobeDCT(), getExif());
return new JPEGImage10Metadata(segments, getSOF(), getJFIF(), getJFXX(), getEmbeddedICCProfile(true), getAdobeDCT(), parseExif(getExif()));
}
@Override
@@ -1349,24 +1279,7 @@ public final class JPEGImageReader extends ImageReaderBase {
}
}
private class ThumbnailProgressDelegate implements ThumbnailReadProgressListener {
@Override
public void thumbnailStarted(int imageIndex, int thumbnailIndex) {
processThumbnailStarted(imageIndex, thumbnailIndex);
}
@Override
public void thumbnailProgress(float percentageDone) {
processThumbnailProgress(percentageDone);
}
@Override
public void thumbnailComplete() {
processThumbnailComplete();
}
}
private class JPEGSegmentStreamWarningDelegate implements JPEGSegmentStreamWarningListener {
private class JPEGSegmentWarningDelegate implements JPEGSegmentWarningListener {
@Override
public void warningOccurred(String warning) {
processWarningOccurred(warning);
@@ -1392,7 +1305,7 @@ public final class JPEGImageReader extends ImageReaderBase {
final String arg = args[argIdx];
if (arg.charAt(0) == '-') {
if (arg.equals("-s") || arg.equals("--subsample") && args.length > argIdx) {
if (arg.equals("-s") || arg.equals("--subsample") && args.length > argIdx + 1) {
String[] sub = args[++argIdx].split(",");
try {
@@ -1411,7 +1324,7 @@ public final class JPEGImageReader extends ImageReaderBase {
System.err.println("Bad sub sampling (x,y): '" + args[argIdx] + "'");
}
}
else if (arg.equals("-r") || arg.equals("--roi") && args.length > argIdx) {
else if (arg.equals("-r") || arg.equals("--roi") && args.length > argIdx + 1) {
String[] region = args[++argIdx].split(",");
try {

View File

@@ -31,8 +31,6 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.stream.BufferedImageInputStream;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
@@ -77,7 +75,7 @@ final class JPEGLosslessDecoderWrapper {
* @throws IOException is thrown if the decoder failed or a conversion is not supported
*/
BufferedImage readImage(final List<Segment> segments, final ImageInputStream input) throws IOException {
JPEGLosslessDecoder decoder = new JPEGLosslessDecoder(segments, createBufferedInput(input), listenerDelegate);
JPEGLosslessDecoder decoder = new JPEGLosslessDecoder(segments, input, listenerDelegate);
// TODO: Allow 10/12/14 bit (using a ComponentColorModel with correct bits, as in TIFF)
// TODO: Rewrite this to pass a pre-allocated buffer of correct type (byte/short)/correct bands
@@ -111,10 +109,6 @@ final class JPEGLosslessDecoderWrapper {
throw new IIOException("JPEG Lossless with " + decoder.getPrecision() + " bit precision and " + decoder.getNumComponents() + " component(s) not supported");
}
private ImageInputStream createBufferedInput(final ImageInputStream input) throws IOException {
return input instanceof BufferedImageInputStream ? input : new BufferedImageInputStream(input);
}
Raster readRaster(final List<Segment> segments, final ImageInputStream input) throws IOException {
// TODO: Can perhaps be implemented faster
return readImage(segments, input).getRaster();

View File

@@ -59,7 +59,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
// TODO: Support multiple JPEG streams (SOI...EOI, SOI...EOI, ...) in a single file
private final ImageInputStream stream;
private final JPEGSegmentStreamWarningListener warningListener;
private final JPEGSegmentWarningListener warningListener;
private final ComponentIdSet componentIds = new ComponentIdSet();
@@ -68,13 +68,13 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
private Segment segment;
JPEGSegmentImageInputStream(final ImageInputStream stream, final JPEGSegmentStreamWarningListener warningListener) {
JPEGSegmentImageInputStream(final ImageInputStream stream, final JPEGSegmentWarningListener warningListener) {
this.stream = notNull(stream, "stream");
this.warningListener = notNull(warningListener, "warningListener");
}
JPEGSegmentImageInputStream(final ImageInputStream stream) {
this(stream, JPEGSegmentStreamWarningListener.NULL_LISTENER);
this(stream, JPEGSegmentWarningListener.NULL_LISTENER);
}
private void processWarningOccured(final String warning) {
@@ -150,7 +150,6 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
else {
if (marker == JPEG.EOI) {
segment = new Segment(marker, realPosition, segment.end(), 2);
segments.add(segment);
}
else {
// Length including length field itself
@@ -165,6 +164,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
// Inspect segment, see if we have 16 bit precision (assuming segments will not contain
// multiple quality tables with varying precision)
int qtInfo = stream.read();
if ((qtInfo & 0x10) == 0x10) {
processWarningOccured("16 bit DQT encountered");
segment = new DownsampledDQTReplacement(realPosition, segment.end(), length, qtInfo, stream);
@@ -188,10 +188,9 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
else {
segment = new Segment(marker, realPosition, segment.end(), length);
}
segments.add(segment);
}
segments.add(segment);
currentSegment = segments.size() - 1;
if (marker == JPEG.SOS) {
@@ -572,12 +571,17 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
@Override
public int read(final ImageInputStream stream) {
return data[pos++] & 0xff;
return data.length > pos ? data[pos++] & 0xff : -1;
}
@Override
public int read(final ImageInputStream stream, byte[] b, int off, int len) {
int length = Math.min(data.length - pos, len);
int dataLeft = data.length - pos;
if (dataLeft <= 0) {
return -1;
}
int length = Math.min(dataLeft, len);
System.arraycopy(data, pos, b, off, length);
pos += length;

View File

@@ -33,10 +33,10 @@ package com.twelvemonkeys.imageio.plugins.jpeg;
/**
* JPEGSegmentStreamWarningListener
*/
interface JPEGSegmentStreamWarningListener {
interface JPEGSegmentWarningListener {
void warningOccurred(String warning);
JPEGSegmentStreamWarningListener NULL_LISTENER = new JPEGSegmentStreamWarningListener() {
JPEGSegmentWarningListener NULL_LISTENER = new JPEGSegmentWarningListener() {
@Override
public void warningOccurred(final String warning) {}
};

View File

@@ -31,12 +31,16 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.IOException;
import static com.twelvemonkeys.lang.Validate.isTrue;
import static com.twelvemonkeys.lang.Validate.notNull;
/**
* ThumbnailReader
*
@@ -46,68 +50,156 @@ import java.io.IOException;
*/
abstract class ThumbnailReader {
private final ThumbnailReadProgressListener progressListener;
protected final int imageIndex;
protected final int thumbnailIndex;
protected ThumbnailReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex) {
this.progressListener = progressListener != null ? progressListener : new NullProgressListener();
this.imageIndex = imageIndex;
this.thumbnailIndex = thumbnailIndex;
}
protected final void processThumbnailStarted() {
progressListener.thumbnailStarted(imageIndex, thumbnailIndex);
}
protected final void processThumbnailProgress(float percentageDone) {
progressListener.thumbnailProgress(percentageDone);
}
protected final void processThumbnailComplete() {
progressListener.thumbnailComplete();
}
static protected BufferedImage readJPEGThumbnail(final ImageReader reader, final ImageInputStream stream) throws IOException {
reader.setInput(stream);
return reader.read(0);
}
static protected BufferedImage readRawThumbnail(final byte[] thumbnail, final int size, final int offset, int w, int h) {
DataBufferByte buffer = new DataBufferByte(thumbnail, size, offset);
WritableRaster raster;
ColorModel cm;
if (thumbnail.length == w * h) {
raster = Raster.createInterleavedRaster(buffer, w, h, w, 1, new int[] {0}, null);
cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
}
else {
raster = Raster.createInterleavedRaster(buffer, w, h, w * 3, 3, new int[] {0, 1, 2}, null);
cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
}
return new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
}
public abstract BufferedImage read() throws IOException;
public abstract int getWidth() throws IOException;
public abstract int getHeight() throws IOException;
private static class NullProgressListener implements ThumbnailReadProgressListener {
@Override
public void thumbnailStarted(int imageIndex, int thumbnailIndex) {
public IIOMetadata readMetadata() throws IOException {
return null;
}
static class UncompressedThumbnailReader extends ThumbnailReader {
private final int width;
private final int height;
private final byte[] data;
private final int offset;
public UncompressedThumbnailReader(int width, int height, byte[] data) {
this(width, height, data, 0);
}
public UncompressedThumbnailReader(int width, int height, byte[] data, int offset) {
this.width = isTrue(width > 0, width, "width");
this.height = isTrue(height > 0, height, "height");;
this.data = notNull(data, "data");
this.offset = isTrue(offset >= 0 && offset < data.length, offset, "offset");
}
@Override
public void thumbnailProgress(float percentageDone) {
public BufferedImage read() throws IOException {
DataBufferByte buffer = new DataBufferByte(data, data.length, offset);
WritableRaster raster;
ColorModel cm;
if (data.length == width * height) {
raster = Raster.createInterleavedRaster(buffer, width, height, width, 1, new int[] {0}, null);
cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
}
else {
raster = Raster.createInterleavedRaster(buffer, width, height, width * 3, 3, new int[] {0, 1, 2}, null);
cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
}
return new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
}
@Override
public void thumbnailComplete() {
public int getWidth() throws IOException {
return width;
}
@Override
public int getHeight() throws IOException {
return height;
}
}
static class IndexedThumbnailReader extends ThumbnailReader {
private final int width;
private final int height;
private final byte[] palette;
private final int paletteOff;
private final byte[] data;
private final int dataOff;
public IndexedThumbnailReader(final int width, int height, final byte[] palette, final int paletteOff, final byte[] data, final int dataOff) {
this.width = isTrue(width > 0, width, "width");
this.height = isTrue(height > 0, height, "height");;
this.palette = notNull(palette, "palette");
this.paletteOff = isTrue(paletteOff >= 0 && paletteOff < palette.length, paletteOff, "paletteOff");
this.data = notNull(data, "data");
this.dataOff = isTrue(dataOff >= 0 && dataOff < data.length, dataOff, "dataOff");
}
@Override
public BufferedImage read() throws IOException {
// 256 RGB triplets
int[] rgbs = new int[256];
for (int i = 0; i < rgbs.length; i++) {
rgbs[i] = (palette[paletteOff + 3 * i ] & 0xff) << 16
| (palette[paletteOff + 3 * i + 1] & 0xff) << 8
| (palette[paletteOff + 3 * i + 2] & 0xff);
}
IndexColorModel icm = new IndexColorModel(8, rgbs.length, rgbs, 0, false, -1, DataBuffer.TYPE_BYTE);
DataBufferByte buffer = new DataBufferByte(data, data.length - dataOff, dataOff);
WritableRaster raster = Raster.createPackedRaster(buffer, width, height, 8, null);
return new BufferedImage(icm, raster, icm.isAlphaPremultiplied(), null);
}
@Override
public int getWidth() throws IOException {
return width;
}
@Override
public int getHeight() throws IOException {
return height;
}
}
static class JPEGThumbnailReader extends ThumbnailReader {
private final ImageReader reader;
private final ImageInputStream input;
private final long offset;
private Dimension dimension;
public JPEGThumbnailReader(final ImageReader reader, final ImageInputStream input, final long offset) {
this.reader = notNull(reader, "reader");
this.input = notNull(input, "input");
this.offset = isTrue(offset >= 0, offset, "offset");
}
private void initReader() throws IOException {
if (reader.getInput() != input) {
input.seek(offset);
reader.setInput(input);
}
}
@Override
public BufferedImage read() throws IOException {
initReader();
return reader.read(0, null);
}
private Dimension readDimensions() throws IOException {
if (dimension == null) {
initReader();
dimension = new Dimension(reader.getWidth(0), reader.getHeight(0));
}
return dimension;
}
@Override
public int getWidth() throws IOException {
return readDimensions().width;
}
@Override
public int getHeight() throws IOException {
return readDimensions().height;
}
@Override
public IIOMetadata readMetadata() throws IOException {
initReader();
return reader.getImageMetadata(0);
}
}
}

View File

@@ -52,9 +52,7 @@ public abstract class AbstractThumbnailReaderTest {
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
}
protected abstract ThumbnailReader createReader(
ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, ImageInputStream stream
) throws IOException;
protected abstract ThumbnailReader createReader(ImageInputStream stream) throws IOException;
protected final ImageInputStream createStream(final String name) throws IOException {
URL resource = getClass().getResource(name);

View File

@@ -30,23 +30,33 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.metadata.AbstractCompoundDirectory;
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
import com.twelvemonkeys.imageio.metadata.tiff.IFD;
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
import org.junit.Test;
import org.mockito.InOrder;
import org.junit.After;
import org.junit.Test;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* EXIFThumbnailReaderTest
@@ -57,31 +67,175 @@ import static org.mockito.Mockito.*;
*/
public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
private final ImageReader thumbnailReader = ImageIO.getImageReadersByFormatName("jpeg").next();
@After
public void tearDown() {
thumbnailReader.dispose();
}
@Test
public void testFromNullSegment() throws IOException {
assertNull(EXIFThumbnail.from(null, null, thumbnailReader));
}
@Test
public void testFromNullIFD() throws IOException {
assertNull(EXIFThumbnail.from(new EXIF(new byte[0]), null, thumbnailReader));
}
@Test
public void testFromEmptyIFD() throws IOException {
assertNull(EXIFThumbnail.from(new EXIF(new byte[0]), new EXIFDirectory(), thumbnailReader));
}
@Test
public void testFromSingleIFD() throws IOException {
assertNull(EXIFThumbnail.from(new EXIF(new byte[42]), new EXIFDirectory(new IFD(Collections.<Entry>emptyList())), thumbnailReader));
}
@Test(expected = IIOException.class)
public void testFromMissingThumbnail() throws IOException {
EXIFThumbnail.from(new EXIF(new byte[42]), new EXIFDirectory(new IFD(Collections.<Entry>emptyList()), new IFD(Collections.<Entry>emptyList())), thumbnailReader);
}
@Test(expected = IIOException.class)
public void testFromUnsupportedThumbnailCompression() throws IOException {
List<TIFFEntry> entries = Collections.singletonList(new TIFFEntry(TIFF.TAG_COMPRESSION, 42));
EXIFThumbnail.from(new EXIF(new byte[42]), new EXIFDirectory(new IFD(Collections.<Entry>emptyList()), new IFD(entries)), thumbnailReader);
}
@Test(expected = IIOException.class)
public void testFromMissingOffsetUncompressed() throws IOException {
List<TIFFEntry> entries = Arrays.asList(
new TIFFEntry(TIFF.TAG_COMPRESSION, 1),
new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, 16),
new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, 9)
);
EXIFThumbnail.from(new EXIF(new byte[6 + 16 * 9 * 3]), new EXIFDirectory(new IFD(Collections.<Entry>emptyList()), new IFD(entries)), thumbnailReader);
}
@Test(expected = IIOException.class)
public void testFromMissingWidthUncompressed() throws IOException {
List<TIFFEntry> entries = Arrays.asList(
new TIFFEntry(TIFF.TAG_COMPRESSION, 1),
new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, 0),
new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, 9)
);
EXIFThumbnail.from(new EXIF(new byte[6 + 16 * 9 * 3]), new EXIFDirectory(new IFD(Collections.<Entry>emptyList()), new IFD(entries)), thumbnailReader);
}
@Test(expected = IIOException.class)
public void testFromMissingHeightUncompressed() throws IOException {
List<TIFFEntry> entries = Arrays.asList(
new TIFFEntry(TIFF.TAG_COMPRESSION, 1),
new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, 0),
new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, 16)
);
EXIFThumbnail.from(new EXIF(new byte[6 + 16 * 9 * 3]), new EXIFDirectory(new IFD(Collections.<Entry>emptyList()), new IFD(entries)), thumbnailReader);
}
@Test(expected = IIOException.class)
public void testFromUnsupportedPhotometricUncompressed() throws IOException {
List<TIFFEntry> entries = Arrays.asList(
new TIFFEntry(TIFF.TAG_COMPRESSION, 1),
new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, 0),
new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, 16),
new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, 9),
new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, 42)
);
EXIFThumbnail.from(new EXIF(new byte[6 + 16 * 9 * 3]), new EXIFDirectory(new IFD(Collections.<Entry>emptyList()), new IFD(entries)), thumbnailReader);
}
@Test(expected = IIOException.class)
public void testFromUnsupportedBitsPerSampleUncompressed() throws IOException {
List<TIFFEntry> entries = Arrays.asList(
new TIFFEntry(TIFF.TAG_COMPRESSION, 1),
new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, 0),
new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, 16),
new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, 9),
new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, new int[]{5, 6, 5})
);
EXIFThumbnail.from(new EXIF(new byte[6 + 16 * 9 * 3]), new EXIFDirectory(new IFD(Collections.<Entry>emptyList()), new IFD(entries)), thumbnailReader);
}
@Test(expected = IIOException.class)
public void testFromUnsupportedSamplesPerPixelUncompressed() throws IOException {
List<TIFFEntry> entries = Arrays.asList(
new TIFFEntry(TIFF.TAG_COMPRESSION, 1),
new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, 0),
new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, 160),
new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, 90),
new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1)
);
EXIFThumbnail.from(new EXIF(new byte[6 + 16 * 9]), new EXIFDirectory(new IFD(Collections.<Entry>emptyList()), new IFD(entries)), thumbnailReader);
}
@Test(expected = IIOException.class)
public void testFromTruncatedUncompressed() throws IOException {
List<TIFFEntry> entries = Arrays.asList(
new TIFFEntry(TIFF.TAG_COMPRESSION, 1),
new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, 0),
new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, 160),
new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, 90)
);
EXIFThumbnail.from(new EXIF(new byte[42]), new EXIFDirectory(new IFD(Collections.<Entry>emptyList()), new IFD(entries)), thumbnailReader);
}
@Test
public void testValidUncompressed() throws IOException {
List<TIFFEntry> entries = Arrays.asList(
new TIFFEntry(TIFF.TAG_COMPRESSION, 1),
new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, 0),
new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, 16),
new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, 9)
);
ThumbnailReader reader = EXIFThumbnail.from(new EXIF(new byte[6 + 16 * 9 * 3]), new EXIFDirectory(new IFD(Collections.<Entry>emptyList()), new IFD(entries)), thumbnailReader);
assertNotNull(reader);
// Sanity check below
assertEquals(16, reader.getWidth());
assertEquals(9, reader.getHeight());
assertNotNull(reader.read());
}
@Test(expected = IIOException.class)
public void testFromMissingOffsetJPEG() throws IOException {
List<TIFFEntry> entries = Collections.singletonList(new TIFFEntry(TIFF.TAG_COMPRESSION, 6));
EXIFThumbnail.from(new EXIF(new byte[42]), new EXIFDirectory(new IFD(Collections.<Entry>emptyList()), new IFD(entries)), thumbnailReader);
}
@Test(expected = IIOException.class)
public void testFromTruncatedJPEG() throws IOException {
List<TIFFEntry> entries = Arrays.asList(
new TIFFEntry(TIFF.TAG_COMPRESSION, 6),
new TIFFEntry(TIFF.TAG_JPEG_INTERCHANGE_FORMAT, 0)
);
EXIFThumbnail.from(new EXIF(new byte[42]), new EXIFDirectory(new IFD(Collections.<Entry>emptyList()), new IFD(entries)), thumbnailReader);
}
@Override
protected EXIFThumbnailReader createReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex, final ImageInputStream stream) throws IOException {
protected ThumbnailReader createReader(final ImageInputStream stream) throws IOException {
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP1, "Exif");
stream.close();
assertNotNull(segments);
assertFalse(segments.isEmpty());
TIFFReader reader = new TIFFReader();
InputStream data = segments.get(0).data();
if (data.read() < 0) {
throw new AssertionError("EOF!");
}
JPEGSegment exifSegment = segments.get(0);
InputStream data = exifSegment.segmentData();
byte[] exifData = new byte[exifSegment.segmentLength() - 2];
new DataInputStream(data).readFully(exifData);
ImageInputStream exifStream = ImageIO.createImageInputStream(data);
CompoundDirectory ifds = (CompoundDirectory) reader.read(exifStream);
assertEquals(2, ifds.directoryCount());
return new EXIFThumbnailReader(progressListener, ImageIO.getImageReadersByFormatName("JPEG").next(), imageIndex, thumbnailIndex, ifds.getDirectory(1), exifStream);
EXIF exif = new EXIF(exifData);
return EXIFThumbnail.from(exif, (CompoundDirectory) new TIFFReader().read(exif.exifData()), thumbnailReader);
}
@Test
public void testReadJPEG() throws IOException {
ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/cmyk-sample-multiple-chunk-icc.jpg"));
ThumbnailReader reader = createReader(createStream("/jpeg/cmyk-sample-multiple-chunk-icc.jpg"));
assertEquals(114, reader.getWidth());
assertEquals(160, reader.getHeight());
@@ -94,7 +248,7 @@ public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
@Test
public void testReadRaw() throws IOException {
ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/exif-rgb-thumbnail-sony-d700.jpg"));
ThumbnailReader reader = createReader(createStream("/jpeg/exif-rgb-thumbnail-sony-d700.jpg"));
assertEquals(80, reader.getWidth());
assertEquals(60, reader.getHeight());
@@ -105,27 +259,9 @@ public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
assertEquals(60, thumbnail.getHeight());
}
@Test
public void testProgressListenerJPEG() throws IOException {
ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class);
createReader(listener, 42, 43, createStream("/jpeg/cmyk-sample-multiple-chunk-icc.jpg")).read();
InOrder order = inOrder(listener);
order.verify(listener).thumbnailStarted(42, 43);
order.verify(listener, atLeastOnce()).thumbnailProgress(100f);
order.verify(listener).thumbnailComplete();
}
@Test
public void testProgressListenerRaw() throws IOException {
ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class);
createReader(listener, 0, 99, createStream("/jpeg/exif-rgb-thumbnail-sony-d700.jpg")).read();
InOrder order = inOrder(listener);
order.verify(listener).thumbnailStarted(0, 99);
order.verify(listener, atLeastOnce()).thumbnailProgress(100f);
order.verify(listener).thumbnailComplete();
private static class EXIFDirectory extends AbstractCompoundDirectory {
public EXIFDirectory(IFD... ifds) {
super(Arrays.asList(ifds));
}
}
}

View File

@@ -33,9 +33,10 @@ package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
import org.junit.Test;
import org.mockito.InOrder;
import org.junit.Test;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.DataInputStream;
@@ -43,7 +44,6 @@ import java.io.IOException;
import java.util.List;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* JFIFThumbnailReaderTest
@@ -53,8 +53,9 @@ import static org.mockito.Mockito.*;
* @version $Id: JFIFThumbnailReaderTest.java,v 1.0 04.05.12 15:56 haraldk Exp$
*/
public class JFIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
@Override
protected JFIFThumbnailReader createReader(ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, ImageInputStream stream) throws IOException {
protected ThumbnailReader createReader(ImageInputStream stream) throws IOException {
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP0, "JFIF");
stream.close();
@@ -62,12 +63,44 @@ public class JFIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
assertFalse(segments.isEmpty());
JPEGSegment segment = segments.get(0);
return new JFIFThumbnailReader(progressListener, imageIndex, thumbnailIndex, JFIF.read(new DataInputStream(segment.segmentData()), segment.segmentLength()));
return JFIFThumbnail.from(JFIF.read(new DataInputStream(segment.segmentData()), segment.segmentLength()));
}
@Test
public void testFromNull() throws IOException {
assertNull(JFIFThumbnail.from(null));
}
@Test
public void testFromNullThumbnail() throws IOException {
assertNull(JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 0, 0, null)));
}
@Test
public void testFromEmpty() throws IOException {
assertNull(JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 0, 0, new byte[0])));
}
@Test(expected = IIOException.class)
public void testFromTruncated() throws IOException {
JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 255, 170, new byte[99]));
}
@Test
public void testFromValid() throws IOException {
ThumbnailReader reader = JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 30, 20, new byte[30 * 20 * 3]));
assertNotNull(reader);
// Sanity check below
assertEquals(30, reader.getWidth());
assertEquals(20, reader.getHeight());
assertNotNull(reader.read());
}
@Test
public void testReadRaw() throws IOException {
ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/jfif-jfif-and-exif-thumbnail-sharpshot-iphone.jpg"));
ThumbnailReader reader = createReader(createStream("/jpeg/jfif-jfif-and-exif-thumbnail-sharpshot-iphone.jpg"));
assertEquals(131, reader.getWidth());
assertEquals(122, reader.getHeight());
@@ -80,7 +113,7 @@ public class JFIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
@Test
public void testReadNonSpecGray() throws IOException {
ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/jfif-grayscale-thumbnail.jpg"));
ThumbnailReader reader = createReader(createStream("/jpeg/jfif-grayscale-thumbnail.jpg"));
assertEquals(127, reader.getWidth());
assertEquals(76, reader.getHeight());
@@ -91,16 +124,4 @@ public class JFIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
assertEquals(127, thumbnail.getWidth());
assertEquals(76, thumbnail.getHeight());
}
@Test
public void testProgressListenerRaw() throws IOException {
ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class);
createReader(listener, 0, 99, createStream("/jpeg/jfif-jfif-and-exif-thumbnail-sharpshot-iphone.jpg")).read();
InOrder order = inOrder(listener);
order.verify(listener).thumbnailStarted(0, 99);
order.verify(listener, atLeastOnce()).thumbnailProgress(100f);
order.verify(listener).thumbnailComplete();
}
}

View File

@@ -33,10 +33,13 @@ package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
import org.junit.Test;
import org.mockito.InOrder;
import org.junit.After;
import org.junit.Test;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.DataInputStream;
@@ -44,7 +47,6 @@ import java.io.IOException;
import java.util.List;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* JFXXThumbnailReaderTest
@@ -54,8 +56,10 @@ import static org.mockito.Mockito.*;
* @version $Id: JFXXThumbnailReaderTest.java,v 1.0 04.05.12 15:56 haraldk Exp$
*/
public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest {
private final ImageReader thumbnailReader = ImageIO.getImageReadersByFormatName("jpeg").next();
@Override
protected JFXXThumbnailReader createReader(ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, ImageInputStream stream) throws IOException {
protected ThumbnailReader createReader(final ImageInputStream stream) throws IOException {
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP0, "JFXX");
stream.close();
@@ -63,12 +67,69 @@ public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest {
assertFalse(segments.isEmpty());
JPEGSegment jfxx = segments.get(0);
return new JFXXThumbnailReader(progressListener, ImageIO.getImageReadersByFormatName("jpeg").next(), imageIndex, thumbnailIndex, JFXX.read(new DataInputStream(jfxx.segmentData()), jfxx.length()));
return JFXXThumbnail.from(JFXX.read(new DataInputStream(jfxx.segmentData()), jfxx.length()), thumbnailReader);
}
@After
public void tearDown() {
thumbnailReader.dispose();
}
@Test
public void testFromNull() throws IOException {
assertNull(JFXXThumbnail.from(null, thumbnailReader));
}
@Test(expected = IIOException.class)
public void testFromNullThumbnail() throws IOException {
JFXXThumbnail.from(new JFXX(JFXX.JPEG, null), thumbnailReader);
}
@Test(expected = IIOException.class)
public void testFromEmpty() throws IOException {
JFXXThumbnail.from(new JFXX(JFXX.JPEG, new byte[0]), thumbnailReader);
}
@Test(expected = IIOException.class)
public void testFromTruncatedJPEG() throws IOException {
JFXXThumbnail.from(new JFXX(JFXX.JPEG, new byte[99]), thumbnailReader);
}
@Test(expected = IIOException.class)
public void testFromTruncatedRGB() throws IOException {
byte[] thumbnail = new byte[765];
thumbnail[0] = (byte) 160;
thumbnail[1] = 90;
JFXXThumbnail.from(new JFXX(JFXX.RGB, thumbnail), thumbnailReader);
}
@Test(expected = IIOException.class)
public void testFromTruncatedIndexed() throws IOException {
byte[] thumbnail = new byte[365];
thumbnail[0] = (byte) 160;
thumbnail[1] = 90;
JFXXThumbnail.from(new JFXX(JFXX.INDEXED, thumbnail), thumbnailReader);
}
@Test
public void testFromValid() throws IOException {
byte[] thumbnail = new byte[14];
thumbnail[0] = 2;
thumbnail[1] = 2;
ThumbnailReader reader = JFXXThumbnail.from(new JFXX(JFXX.RGB, thumbnail), thumbnailReader);
assertNotNull(reader);
// Sanity check below
assertEquals(2, reader.getWidth());
assertEquals(2, reader.getHeight());
assertNotNull(reader.read());
}
@Test
public void testReadJPEG() throws IOException {
ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg"));
ThumbnailReader reader = createReader(createStream("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg"));
assertEquals(80, reader.getWidth());
assertEquals(60, reader.getHeight());
@@ -81,16 +142,4 @@ public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest {
// TODO: Test JFXX indexed thumbnail
// TODO: Test JFXX RGB thumbnail
@Test
public void testProgressListenerRaw() throws IOException {
ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class);
createReader(listener, 0, 99, createStream("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg")).read();
InOrder order = inOrder(listener);
order.verify(listener).thumbnailStarted(0, 99);
order.verify(listener, atLeastOnce()).thumbnailProgress(100f);
order.verify(listener).thumbnailComplete();
}
}

View File

@@ -61,6 +61,8 @@ import java.util.List;
import java.util.*;
import static com.twelvemonkeys.imageio.util.IIOUtil.lookupProviderByName;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeNoException;
@@ -1415,7 +1417,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest<JPEGImageReader
assertNotNull(unknown.getUserObject()); // All unknowns must have user object (data array)
}
}
catch (IIOException e) {
catch (IOException e) {
e.printStackTrace();
fail(String.format("Reading metadata failed for %s image %s: %s", testData, i, e.getMessage()));
}
@@ -1959,4 +1961,36 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest<JPEGImageReader
reader.dispose();
}
}
@Test(timeout = 1000L)
public void testInfiniteLoopCorrupt() throws IOException {
ImageReader reader = createReader();
try (ImageInputStream iis = ImageIO.createImageInputStream(getClassLoaderResource("/broken-jpeg/110115680-6d6dce80-7d84-11eb-99df-4cb21df3b09f.jpeg"))) {
reader.setInput(iis);
try {
reader.read(0, null);
}
catch (IIOException expected) {
assertThat(expected.getMessage(), allOf(containsString("SOF"), containsString("stream")));
}
}
}
@Test(timeout = 1000L)
public void testInfiniteLoopCorruptRaster() throws IOException {
ImageReader reader = createReader();
try (ImageInputStream iis = ImageIO.createImageInputStream(getClassLoaderResource("/broken-jpeg/110115680-6d6dce80-7d84-11eb-99df-4cb21df3b09f.jpeg"))) {
reader.setInput(iis);
try {
reader.readRaster(0, null);
}
catch (IIOException expected) {
assertThat(expected.getMessage(), allOf(containsString("SOF"), containsString("stream")));
}
}
}
}

View File

@@ -187,7 +187,7 @@ public class JPEGSegmentImageInputStreamTest {
assertEquals(2, iis.read(buffer, 0, buffer.length));
assertEquals(2, iis.getStreamPosition());
iis.seek(2000); // Just a random postion beyond EOF
iis.seek(2000); // Just a random position beyond EOF
assertEquals(2000, iis.getStreamPosition());
// So far, so good (but stream position is now really beyond EOF)...
@@ -228,4 +228,29 @@ public class JPEGSegmentImageInputStreamTest {
assertEquals(-1, iis.read());
assertEquals(0x2012, iis.getStreamPosition());
}
@Test(timeout = 1000L)
public void testInfiniteLoopCorrupt() throws IOException {
try (ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/broken-jpeg/110115680-6d6dce80-7d84-11eb-99df-4cb21df3b09f.jpeg")))) {
long length = 0;
while (stream.read() != -1) {
length++;
}
assertEquals(25504L, length); // Sanity check: same as file size, except..?
}
try (ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/broken-jpeg/110115680-6d6dce80-7d84-11eb-99df-4cb21df3b09f.jpeg")))) {
long length = 0;
byte[] buffer = new byte[1024];
int read;
while ((read = stream.read(buffer)) != -1) {
length += read;
}
assertEquals(25504L, length); // Sanity check: same as file size, except..?
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -48,7 +48,7 @@ import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
import static com.twelvemonkeys.lang.Validate.notNull;
@@ -154,7 +154,7 @@ public final class JPEGSegmentUtil {
}
static String asAsciiString(final byte[] data, final int offset, final int length) {
return new String(data, offset, length, Charset.forName("ascii"));
return new String(data, offset, length, StandardCharsets.US_ASCII);
}
static void readSOI(final ImageInputStream stream) throws IOException {
@@ -348,6 +348,7 @@ public final class JPEGSegmentUtil {
else if ("Photoshop 3.0".equals(segment.identifier())) {
// TODO: The "Photoshop 3.0" segment contains several image resources, of which one might contain
// IPTC metadata. Probably duplicated in the XMP though...
// TODO: Merge multiple APP13 segments to single resource block
ImageInputStream stream = new ByteArrayImageInputStream(segment.data, segment.offset(), segment.length());
Directory psd = new PSDReader().read(stream);
Entry iccEntry = psd.getEntryById(PSD.RES_ICC_PROFILE);
@@ -359,6 +360,7 @@ public final class JPEGSegmentUtil {
System.err.println(TIFFReader.HexDump.dump(segment.data));
}
else if ("ICC_PROFILE".equals(segment.identifier())) {
// TODO: Merge multiple APP2 segments to single ICC Profile
// Skip
}
else {

View File

@@ -30,13 +30,13 @@
package com.twelvemonkeys.imageio.metadata.xmp;
import com.twelvemonkeys.imageio.stream.BufferedImageInputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* XMPScanner
@@ -101,14 +101,10 @@ public final class XMPScanner {
* @throws IOException if an I/O exception occurs reading from {@code pInput}.
* @see ImageIO#createImageInputStream(Object)
*/
@SuppressWarnings("StatementWithEmptyBody")
static public Reader scanForXMPPacket(final Object pInput) throws IOException {
ImageInputStream stream = pInput instanceof ImageInputStream ? (ImageInputStream) pInput : ImageIO.createImageInputStream(pInput);
// TODO: Consider if BufferedIIS is a good idea
if (!(stream instanceof BufferedImageInputStream)) {
stream = new BufferedImageInputStream(stream);
}
// TODO: Might be more than one XMP block per file (it's possible to re-start for now)..
long pos;
pos = scanForSequence(stream, XMP_PACKET_BEGIN);
@@ -128,17 +124,17 @@ public final class XMPScanner {
if (bom[0] == (byte) 0xEF && bom[1] == (byte) 0xBB && bom[2] == (byte) 0xBF && bom[3] == quote ||
bom[0] == quote) {
// UTF-8
cs = Charset.forName("UTF-8");
cs = StandardCharsets.UTF_8;
}
else if (bom[0] == (byte) 0xFE && bom[1] == (byte) 0xFF && bom[2] == 0x00 && bom[3] == quote) {
// UTF-16 BIG endian
cs = Charset.forName("UTF-16BE");
cs = StandardCharsets.UTF_16BE;
}
else if (bom[0] == 0x00 && bom[1] == (byte) 0xFF && bom[2] == (byte) 0xFE && bom[3] == quote) {
stream.skipBytes(1); // Alignment
// UTF-16 little endian
cs = Charset.forName("UTF-16LE");
cs = StandardCharsets.UTF_16LE;
}
else if (bom[0] == 0x00 && bom[1] == 0x00 && bom[2] == (byte) 0xFE && bom[3] == (byte) 0xFF) {
// NOTE: 32-bit character set not supported by default
@@ -186,7 +182,7 @@ public final class XMPScanner {
* @throws IOException if an I/O exception occurs during scanning
*/
private static long scanForSequence(final ImageInputStream pStream, final byte[] pSequence) throws IOException {
long start = -1l;
long start = -1L;
int index = 0;
int nullBytes = 0;
@@ -222,7 +218,7 @@ public final class XMPScanner {
}
}
return -1l;
return -1L;
}
public static void main(final String[] pArgs) throws IOException {

View File

@@ -64,7 +64,7 @@ public class PCXImageReaderTest extends ImageReaderAbstractTest<PCXImageReader>
@Override
protected List<TestData> getTestData() {
return Arrays.asList(
new TestData(getClassLoaderResource("/pcx/MARBLES.PCX"), new Dimension(1419, 1001)), // RLE encoded RGB
new TestData(getClassLoaderResource("/pcx/input.pcx"), new Dimension(70, 46)), // RLE encoded RGB
new TestData(getClassLoaderResource("/pcx/lena.pcx"), new Dimension(512, 512)), // RLE encoded RGB
new TestData(getClassLoaderResource("/pcx/lena2.pcx"), new Dimension(512, 512)), // RLE encoded, 256 color indexed (8 bps/1 channel)
new TestData(getClassLoaderResource("/pcx/lena3.pcx"), new Dimension(512, 512)), // RLE encoded, 16 color indexed (4 bps/1 channel)
@@ -76,6 +76,7 @@ public class PCXImageReaderTest extends ImageReaderAbstractTest<PCXImageReader>
new TestData(getClassLoaderResource("/pcx/lena9.pcx"), new Dimension(512, 512)), // RLE encoded, 2 color indexed (1 bps/1 channel)
new TestData(getClassLoaderResource("/pcx/lena10.pcx"), new Dimension(512, 512)), // RLE encoded, 16 color indexed (4 bps/1 channel) (uses only 8 colors)
new TestData(getClassLoaderResource("/pcx/DARKSTAR.PCX"), new Dimension(88, 52)), // RLE encoded monochrome (1 bps/1 channel)
new TestData(getClassLoaderResource("/pcx/MARBLES.PCX"), new Dimension(1419, 1001)), // RLE encoded RGB
new TestData(getClassLoaderResource("/pcx/no-palette-monochrome.pcx"), new Dimension(128, 152)), // RLE encoded monochrome (1 bps/1 channel)
// See cga-pcx.txt, however, the text seems to be in error, the bits can not not as described
new TestData(getClassLoaderResource("/pcx/CGA_BW.PCX"), new Dimension(640, 200)), // RLE encoded indexed (CGA mode)

Binary file not shown.

View File

@@ -68,6 +68,7 @@ import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder;
import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
@@ -76,11 +77,8 @@ import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.image.*;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.*;
/**
* Reader for Apple Mac Paint Picture (PICT) format.
@@ -123,10 +121,11 @@ public final class PICTImageReader extends ImageReaderBase {
private double screenImageYRatio;
// List of images created during image import
private List<BufferedImage> images = new ArrayList<>();
private final List<BufferedImage> images = new ArrayList<>();
private long imageStartStreamPos;
protected int picSize;
@Deprecated
public PICTImageReader() {
this(null);
}
@@ -168,14 +167,14 @@ public final class PICTImageReader extends ImageReaderBase {
* @throws IOException if an I/O error occurs while reading the image.
*/
private void readPICTHeader(final ImageInputStream pStream) throws IOException {
pStream.seek(0l);
pStream.seek(0L);
try {
readPICTHeader0(pStream);
}
catch (IIOException e) {
// Rest and try again
pStream.seek(0l);
pStream.seek(0L);
// Skip first 512 bytes
PICTImageReaderSpi.skipNullHeader(pStream);
@@ -207,7 +206,7 @@ public final class PICTImageReader extends ImageReaderBase {
System.out.println("frame: " + frame);
}
// Set default display ratios. 72 dpi is the standard Macintosh resolution.
// Set default display ratios. 72 dpi is the standard Mac resolution.
screenImageXRatio = 1.0;
screenImageYRatio = 1.0;
@@ -215,7 +214,7 @@ public final class PICTImageReader extends ImageReaderBase {
boolean isExtendedV2 = false;
int version = pStream.readShort();
if (DEBUG) {
System.out.println(String.format("PICT version: 0x%04x", version));
System.out.printf("PICT version: 0x%04x%n", version);
}
if (version == (PICT.OP_VERSION << 8) + 0x01) {
@@ -231,24 +230,20 @@ public final class PICTImageReader extends ImageReaderBase {
int headerVersion = pStream.readInt();
if (DEBUG) {
System.out.println(String.format("headerVersion: 0x%04x", headerVersion));
System.out.printf("headerVersion: 0x%04x%n", headerVersion);
}
// TODO: This (headerVersion) should be picture size (bytes) for non-V2-EXT...?
// - but.. We should take care to make sure we don't mis-interpret non-PICT data...
//if (headerVersion == PICT.HEADER_V2) {
if ((headerVersion & 0xffff0000) != PICT.HEADER_V2_EXT) {
// TODO: Test this.. Looks dodgy to me..
// Get the image resolution and calculate the ratio between
// the default Mac screen resolution and the image resolution
// int y (fixed point)
// int y, x, w(?), h (fixed point)
double y2 = PICTUtil.readFixedPoint(pStream);
// int x (fixed point)
double x2 = PICTUtil.readFixedPoint(pStream);
// int w (fixed point)
double w2 = PICTUtil.readFixedPoint(pStream); // ?!
// int h (fixed point)
double h2 = PICTUtil.readFixedPoint(pStream);
screenImageXRatio = (w - x) / (w2 - x2);
@@ -264,7 +259,7 @@ public final class PICTImageReader extends ImageReaderBase {
// int reserved
pStream.skipBytes(4);
}
else /*if ((headerVersion & 0xffff0000) == PICT.HEADER_V2_EXT)*/ {
else {
isExtendedV2 = true;
// Get the image resolution
// Not sure if they are useful for anything...
@@ -281,13 +276,10 @@ public final class PICTImageReader extends ImageReaderBase {
// Get the image resolution and calculate the ratio between
// the default Mac screen resolution and the image resolution
// short y
// short y, x, h, w
short y2 = pStream.readShort();
// short x
short x2 = pStream.readShort();
// short h
short h2 = pStream.readShort();
// short w
short w2 = pStream.readShort();
screenImageXRatio = (w - x) / (double) (w2 - x2);
@@ -400,7 +392,7 @@ public final class PICTImageReader extends ImageReaderBase {
}
break;
case PICT.OP_CLIP_RGN:// OK for RECTS, not for regions yet
case PICT.OP_CLIP_RGN:// OK for RECTs, not for regions yet
// Read the region
if ((region = readRegion(pStream, bounds)) == null) {
throw new IIOException("Could not read region");
@@ -735,12 +727,13 @@ public final class PICTImageReader extends ImageReaderBase {
case 0x25:
case 0x26:
case 0x27:
case 0x2F:
// Apple reserved
dataLength = pStream.readUnsignedShort();
pStream.readFully(new byte[dataLength], 0, dataLength);
if (DEBUG) {
System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode));
System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode);
}
break;
@@ -829,14 +822,6 @@ public final class PICTImageReader extends ImageReaderBase {
}
break;
case 0x2F:
dataLength = pStream.readUnsignedShort();
pStream.readFully(new byte[dataLength], 0, dataLength);
if (DEBUG) {
System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode));
}
break;
//--------------------------------------------------------------------------------
// Rect treatments
//--------------------------------------------------------------------------------
@@ -920,7 +905,7 @@ public final class PICTImageReader extends ImageReaderBase {
case 0x003e:
case 0x003f:
if (DEBUG) {
System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode));
System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode);
}
break;
@@ -1092,7 +1077,7 @@ public final class PICTImageReader extends ImageReaderBase {
case 0x57:
pStream.readFully(new byte[8], 0, 8);
if (DEBUG) {
System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode));
System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode);
}
break;
@@ -1187,7 +1172,7 @@ public final class PICTImageReader extends ImageReaderBase {
case 0x67:
pStream.readFully(new byte[12], 0, 12);
if (DEBUG) {
System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode));
System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode);
}
break;
case 0x6d:
@@ -1195,7 +1180,7 @@ public final class PICTImageReader extends ImageReaderBase {
case 0x6f:
pStream.readFully(new byte[4], 0, 4);
if (DEBUG) {
System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode));
System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode);
}
break;
@@ -1283,7 +1268,7 @@ public final class PICTImageReader extends ImageReaderBase {
case 0x7e:
case 0x7f:
if (DEBUG) {
System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode));
System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode);
}
break;
@@ -1293,7 +1278,7 @@ public final class PICTImageReader extends ImageReaderBase {
// Read the polygon
polygon = readPoly(pStream, bounds);
if (DEBUG) {
System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode));
System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode);
}
break;
@@ -1384,7 +1369,7 @@ public final class PICTImageReader extends ImageReaderBase {
// Read the region
region = readRegion(pStream, bounds);
if (DEBUG) {
System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode));
System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode);
}
break;
@@ -1414,7 +1399,7 @@ public final class PICTImageReader extends ImageReaderBase {
dataLength = pStream.readUnsignedShort();
pStream.readFully(new byte[dataLength], 0, dataLength);
if (DEBUG) {
System.out.println(String.format("%s: 0x%04x - length: %d", PICT.APPLE_USE_RESERVED_FIELD, opCode, dataLength));
System.out.printf("%s: 0x%04x - length: %d%n", PICT.APPLE_USE_RESERVED_FIELD, opCode, dataLength);
}
break;
@@ -1442,7 +1427,7 @@ public final class PICTImageReader extends ImageReaderBase {
dataLength = pStream.readUnsignedShort();
pStream.readFully(new byte[dataLength], 0, dataLength);
if (DEBUG) {
System.out.println(String.format("%s: 0x%04x", PICT.APPLE_USE_RESERVED_FIELD, opCode));
System.out.printf("%s: 0x%04x%n", PICT.APPLE_USE_RESERVED_FIELD, opCode);
}
break;
@@ -1478,7 +1463,7 @@ public final class PICTImageReader extends ImageReaderBase {
// TODO: Read this as well, need test data
dataLength = pStream.readInt();
if (DEBUG) {
System.out.println(String.format("unCompressedQuickTime, length %d", dataLength));
System.out.printf("unCompressedQuickTime, length %d%n", dataLength);
}
pStream.readFully(new byte[dataLength], 0, dataLength);
break;
@@ -1515,7 +1500,7 @@ public final class PICTImageReader extends ImageReaderBase {
}
if (DEBUG) {
System.out.println(String.format("%s: 0x%04x - length: %s", PICT.APPLE_USE_RESERVED_FIELD, opCode, dataLength));
System.out.printf("%s: 0x%04x - length: %s%n", PICT.APPLE_USE_RESERVED_FIELD, opCode, dataLength);
}
if (dataLength != 0) {
@@ -1577,7 +1562,7 @@ public final class PICTImageReader extends ImageReaderBase {
matrix[i] = pStream.readInt();
}
if (DEBUG) {
System.out.println(String.format("matrix: %s", Arrays.toString(matrix)));
System.out.printf("matrix: %s%n", Arrays.toString(matrix));
}
// Matte
@@ -1833,7 +1818,7 @@ public final class PICTImageReader extends ImageReaderBase {
////////////////////////////////////////////////////
// TODO: This works for single image PICTs only...
// However, this is the most common case. Ok for now
processImageProgress(scanline * 100 / srcRect.height);
processImageProgress(scanline * 100 / (float) srcRect.height);
if (abortRequested()) {
processReadAborted();
@@ -2134,7 +2119,7 @@ public final class PICTImageReader extends ImageReaderBase {
////////////////////////////////////////////////////
// TODO: This works for single image PICTs only...
// However, this is the most common case. Ok for now
processImageProgress(scanline * 100 / srcRect.height);
processImageProgress(scanline * 100 / (float) srcRect.height);
if (abortRequested()) {
processReadAborted();
@@ -2626,7 +2611,7 @@ public final class PICTImageReader extends ImageReaderBase {
return getYPtCoord(getPICTFrame().height);
}
public Iterator<ImageTypeSpecifier> getImageTypes(int pIndex) throws IOException {
public Iterator<ImageTypeSpecifier> getImageTypes(int pIndex) {
// TODO: The images look slightly different in Preview.. Could indicate the color space is wrong...
return Collections.singletonList(
ImageTypeSpecifiers.createPacked(
@@ -2636,11 +2621,19 @@ public final class PICTImageReader extends ImageReaderBase {
).iterator();
}
@Override
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
checkBounds(imageIndex);
getPICTFrame(); // TODO: Would probably be better to use readPictHeader here, but it isn't cached
return new PICTMetadata(version, screenImageXRatio, screenImageYRatio);
}
protected static void showIt(final BufferedImage pImage, final String pTitle) {
ImageReaderBase.showIt(pImage, pTitle);
}
public static void main(final String[] pArgs) throws IOException {
public static void main(final String[] pArgs) {
ImageReader reader = new PICTImageReader(new PICTImageReaderSpi());
for (String arg : pArgs) {

View File

@@ -71,7 +71,7 @@ public final class PICTImageReaderSpi extends ImageReaderSpiBase {
try {
if (isPICT(stream)) {
// If PICT Clipping format, return true immediately
// If PICT clipboard format, return true immediately
return true;
}
else {
@@ -154,8 +154,8 @@ public final class PICTImageReaderSpi extends ImageReaderSpiBase {
pStream.skipBytes(PICT.PICT_NULL_HEADER_SIZE);
}
// NOTE: As the PICT format has a very weak identifier, a true return value is not necessarily a PICT...
private boolean isPICT(final ImageInputStream pStream) throws IOException {
// TODO: Need to validate better...
// Size may be 0, so we can't use this for validation...
pStream.readUnsignedShort();
@@ -169,8 +169,8 @@ public final class PICTImageReaderSpi extends ImageReaderSpiBase {
return false;
}
// Validate magic
int magic = pStream.readInt();
return (magic & 0xffff0000) == PICT.MAGIC_V1 || magic == PICT.MAGIC_V2;
}
@@ -179,6 +179,6 @@ public final class PICTImageReaderSpi extends ImageReaderSpiBase {
}
public String getDescription(final Locale pLocale) {
return "Apple Mac Paint Picture (PICT) image reader";
return "Apple MacPaint/QuickDraw Picture (PICT) image reader";
}
}

View File

@@ -0,0 +1,92 @@
package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.AbstractMetadata;
import javax.imageio.metadata.IIOMetadataNode;
/**
* PICTMetadata.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PICTMetadata.java,v 1.0 23/03/2021 haraldk Exp$
*/
public class PICTMetadata extends AbstractMetadata {
private final int version;
private final double screenImageXRatio;
private final double screenImageYRatio;
PICTMetadata(final int version, final double screenImageXRatio, final double screenImageYRatio) {
this.version = version;
this.screenImageXRatio = screenImageXRatio;
this.screenImageYRatio = screenImageYRatio;
}
@Override
protected IIOMetadataNode getStandardChromaNode() {
IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
chroma.appendChild(csType);
csType.setAttribute("name", "RGB");
// NOTE: Channels in chroma node reflects channels in color model (see data node, for channels in data)
IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
chroma.appendChild(numChannels);
numChannels.setAttribute("value", "3");
IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero");
chroma.appendChild(blackIsZero);
blackIsZero.setAttribute("value", "TRUE");
return chroma;
}
@Override
protected IIOMetadataNode getStandardDimensionNode() {
if (screenImageXRatio > 0.0d && screenImageYRatio > 0.0d) {
IIOMetadataNode node = new IIOMetadataNode("Dimension");
double ratio = screenImageXRatio / screenImageYRatio;
IIOMetadataNode subNode = new IIOMetadataNode("PixelAspectRatio");
subNode.setAttribute("value", "" + ratio);
node.appendChild(subNode);
return node;
}
return null;
}
@Override
protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode data = new IIOMetadataNode("Data");
// As this is a vector-ish format, with possibly multiple regions of pixel data, this makes no sense... :-P
// This is, however, consistent with the getRawImageTyp/getImageTypes
IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration");
planarConfiguration.setAttribute("value", "PixelInterleaved");
data.appendChild(planarConfiguration);
IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
sampleFormat.setAttribute("value", "UnsignedIntegral");
data.appendChild(sampleFormat);
IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
bitsPerSample.setAttribute("value", "32");
data.appendChild(bitsPerSample);
return data;
}
@Override
protected IIOMetadataNode getStandardDocumentNode() {
IIOMetadataNode document = new IIOMetadataNode("Document");
IIOMetadataNode formatVersion = new IIOMetadataNode("FormatVersion");
document.appendChild(formatVersion);
formatVersion.setAttribute("value", Integer.toString(version));
return document;
}
}

View File

@@ -37,6 +37,7 @@ import java.awt.image.IndexColorModel;
import java.io.DataInput;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
/**
@@ -76,7 +77,8 @@ final class PICTUtil {
static String readIdString(final DataInput pStream) throws IOException {
byte[] bytes = new byte[4];
pStream.readFully(bytes);
return new String(bytes, "ASCII");
return new String(bytes, StandardCharsets.US_ASCII);
}
/**

View File

@@ -30,12 +30,16 @@
package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc;
import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.LittleEndianDataOutputStream;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.nio.charset.StandardCharsets;
/**
* QTBMPDecompressor
@@ -45,28 +49,24 @@ import java.io.*;
* @version $Id: QTBMPDecompressor.java,v 1.0 Feb 16, 2009 9:18:28 PM haraldk Exp$
*/
final class QTBMPDecompressor extends QTDecompressor {
public boolean canDecompress(final QuickTime.ImageDesc pDescription) {
return QuickTime.VENDOR_APPLE.equals(pDescription.compressorVendor) && "WRLE".equals(pDescription.compressorIdentifer)
&& "bmp ".equals(idString(pDescription.extraDesc, 4));
public boolean canDecompress(final ImageDesc description) {
return QuickTime.VENDOR_APPLE.equals(description.compressorVendor)
&& "WRLE".equals(description.compressorIdentifer)
&& "bmp ".equals(idString(description.extraDesc, 4));
}
private static String idString(final byte[] pData, final int pOffset) {
try {
return new String(pData, pOffset, 4, "ASCII");
}
catch (UnsupportedEncodingException e) {
throw new Error("ASCII charset must always be supported", e);
}
private static String idString(final byte[] data, final int offset) {
return new String(data, offset, 4, StandardCharsets.US_ASCII);
}
public BufferedImage decompress(final QuickTime.ImageDesc pDescription, final InputStream pStream) throws IOException {
return ImageIO.read(new SequenceInputStream(fakeBMPHeader(pDescription), pStream));
public BufferedImage decompress(final ImageDesc description, final InputStream stream) throws IOException {
return ImageIO.read(new SequenceInputStream(fakeBMPHeader(description), stream));
}
private InputStream fakeBMPHeader(final QuickTime.ImageDesc pDescription) throws IOException {
private InputStream fakeBMPHeader(final ImageDesc description) throws IOException {
int bmpHeaderSize = 14;
int dibHeaderSize = 12; // 12: OS/2 V1
ByteArrayOutputStream out = new FastByteArrayOutputStream(bmpHeaderSize + dibHeaderSize);
FastByteArrayOutputStream out = new FastByteArrayOutputStream(bmpHeaderSize + dibHeaderSize);
LittleEndianDataOutputStream stream = new LittleEndianDataOutputStream(out);
@@ -74,7 +74,7 @@ final class QTBMPDecompressor extends QTDecompressor {
stream.writeByte('B');
stream.writeByte('M');
stream.writeInt(pDescription.dataSize + bmpHeaderSize + dibHeaderSize); // Data size + BMP header + DIB header
stream.writeInt(description.dataSize + bmpHeaderSize + dibHeaderSize); // Data size + BMP header + DIB header
stream.writeShort(0x0); // Reserved
stream.writeShort(0x0); // Reserved
@@ -84,12 +84,12 @@ final class QTBMPDecompressor extends QTDecompressor {
// DIB header
stream.writeInt(dibHeaderSize); // DIB header size
stream.writeShort(pDescription.width);
stream.writeShort(pDescription.height);
stream.writeShort(description.width);
stream.writeShort(description.height);
stream.writeShort(1); // Planes, only legal value: 1
stream.writeShort(pDescription.depth); // Bit depth
stream.writeShort(description.depth); // Bit depth
return new ByteArrayInputStream(out.toByteArray());
return out.createInputStream();
}
}

View File

@@ -30,6 +30,8 @@
package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
@@ -46,20 +48,20 @@ abstract class QTDecompressor {
* Returns whether this decompressor is capable of decompressing the image
* data described by the given image description.
*
* @param pDescription the image description ({@code 'idsc'} Atom).
* @param description the image description ({@code 'idsc'} Atom).
* @return {@code true} if this decompressor is capable of decompressing
* he data in the given image description, otherwise {@code false}.
*/
public abstract boolean canDecompress(QuickTime.ImageDesc pDescription);
public abstract boolean canDecompress(ImageDesc description);
/**
* Decompresses an image.
*
* @param pDescription the image description ({@code 'idsc'} Atom).
* @param pStream the image data stream
* @param description the image description ({@code 'idsc'} Atom).
* @param stream the image data stream
* @return the decompressed image
*
* @throws java.io.IOException if an I/O exception occurs during reading.
*/
public abstract BufferedImage decompress(QuickTime.ImageDesc pDescription, InputStream pStream) throws IOException;
public abstract BufferedImage decompress(ImageDesc description, InputStream stream) throws IOException;
}

View File

@@ -31,9 +31,14 @@
package com.twelvemonkeys.imageio.plugins.pict;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import static com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc;
/**
* QTGenericDecompressor
@@ -43,11 +48,36 @@ import java.io.InputStream;
* @version $Id: QTGenericDecompressor.java,v 1.0 Feb 16, 2009 9:26:13 PM haraldk Exp$
*/
final class QTGenericDecompressor extends QTDecompressor {
public boolean canDecompress(final QuickTime.ImageDesc pDescription) {
public boolean canDecompress(final ImageDesc description) {
// Instead of testing, we just allow everything, and might eventually fail on decompress later...
return true;
}
public BufferedImage decompress(final QuickTime.ImageDesc pDescription, final InputStream pStream) throws IOException {
return ImageIO.read(pStream);
public BufferedImage decompress(final ImageDesc description, final InputStream stream) throws IOException {
BufferedImage image = ImageIO.read(stream);
if (image == null) {
return readUsingFormatName(description.compressorIdentifer.trim(), stream);
}
return image;
}
private BufferedImage readUsingFormatName(final String formatName, final InputStream stream) throws IOException {
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName(formatName);
if (readers.hasNext()) {
ImageReader reader = readers.next();
try (ImageInputStream input = ImageIO.createImageInputStream(stream)) {
reader.setInput(input);
return reader.read(0);
}
finally {
reader.dispose();
}
}
return null;
}
}

View File

@@ -38,6 +38,9 @@ import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import static com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc;
import static com.twelvemonkeys.imageio.plugins.pict.QuickTime.VENDOR_APPLE;
/**
* QTRAWDecompressor
*
@@ -51,21 +54,17 @@ final class QTRAWDecompressor extends QTDecompressor {
// - Have a look at com.sun.media.imageio.stream.RawImageInputStream...
// TODO: Support different bit depths
public boolean canDecompress(final QuickTime.ImageDesc pDescription) {
return QuickTime.VENDOR_APPLE.equals(pDescription.compressorVendor)
&& "raw ".equals(pDescription.compressorIdentifer)
&& (pDescription.depth == 24 || pDescription.depth == 32);
public boolean canDecompress(final ImageDesc description) {
return VENDOR_APPLE.equals(description.compressorVendor)
&& "raw ".equals(description.compressorIdentifer)
&& (description.depth == 24 || description.depth == 32 || description.depth == 40);
}
public BufferedImage decompress(final QuickTime.ImageDesc pDescription, final InputStream pStream) throws IOException {
byte[] data = new byte[pDescription.dataSize];
public BufferedImage decompress(final ImageDesc description, final InputStream stream) throws IOException {
byte[] data = new byte[description.dataSize];
DataInputStream stream = new DataInputStream(pStream);
try {
stream.readFully(data, 0, pDescription.dataSize);
}
finally {
stream.close();
try (DataInputStream dataStream = new DataInputStream(stream)) {
dataStream.readFully(data, 0, description.dataSize);
}
DataBuffer buffer = new DataBufferByte(data, data.length);
@@ -73,12 +72,12 @@ final class QTRAWDecompressor extends QTDecompressor {
WritableRaster raster;
// TODO: Depth parameter can be 1-32 (color) or 33-40 (gray scale)
switch (pDescription.depth) {
switch (description.depth) {
case 40: // 8 bit gray (untested)
raster = Raster.createInterleavedRaster(
buffer,
pDescription.width, pDescription.height,
pDescription.width, 1,
description.width, description.height,
description.width, 1,
new int[] {0},
null
);
@@ -86,8 +85,8 @@ final class QTRAWDecompressor extends QTDecompressor {
case 24: // 24 bit RGB
raster = Raster.createInterleavedRaster(
buffer,
pDescription.width, pDescription.height,
pDescription.width * 3, 3,
description.width, description.height,
description.width * 3, 3,
new int[] {0, 1, 2},
null
);
@@ -96,9 +95,9 @@ final class QTRAWDecompressor extends QTDecompressor {
// WORKAROUND: There is a bug in the way Java 2D interprets the band offsets in
// Raster.createInterleavedRaster (see below) before Java 6. So, instead of
// passing a correct offset array below, we swap channel 1 & 3 to make it ABGR...
for (int y = 0; y < pDescription.height; y++) {
for (int x = 0; x < pDescription.width; x++) {
int offset = 4 * y * pDescription.width + x * 4;
for (int y = 0; y < description.height; y++) {
for (int x = 0; x < description.width; x++) {
int offset = 4 * y * description.width + x * 4;
byte temp = data[offset + 1];
data[offset + 1] = data[offset + 3];
data[offset + 3] = temp;
@@ -107,21 +106,21 @@ final class QTRAWDecompressor extends QTDecompressor {
raster = Raster.createInterleavedRaster(
buffer,
pDescription.width, pDescription.height,
pDescription.width * 4, 4,
description.width, description.height,
description.width * 4, 4,
new int[] {3, 2, 1, 0}, // B & R mixed up. {1, 2, 3, 0} is correct
null
);
break;
default:
throw new IIOException("Unsupported RAW depth: " + pDescription.depth);
throw new IIOException("Unsupported QuickTime RAW depth: " + description.depth);
}
ColorModel cm = new ComponentColorModel(
pDescription.depth <= 32 ? ColorSpace.getInstance(ColorSpace.CS_sRGB) : ColorSpace.getInstance(ColorSpace.CS_GRAY),
pDescription.depth == 32,
description.depth <= 32 ? ColorSpace.getInstance(ColorSpace.CS_sRGB) : ColorSpace.getInstance(ColorSpace.CS_GRAY),
description.depth == 32,
false,
pDescription.depth == 32 ? Transparency.TRANSLUCENT : Transparency.OPAQUE,
description.depth == 32 ? Transparency.TRANSLUCENT : Transparency.OPAQUE,
DataBuffer.TYPE_BYTE
);

View File

@@ -56,7 +56,7 @@ final class QuickTime {
private static final List<QTDecompressor> sDecompressors = Arrays.asList(
new QTBMPDecompressor(),
new QTRAWDecompressor(),
// The GenericDecompressor must be the last in the list
// The GenericDecompressor MUST be the last in the list, as it claims to read everything...
new QTGenericDecompressor()
);
@@ -87,7 +87,7 @@ final class QuickTime {
kH263CodecType ='h263'
kIndeo4CodecType ='IV41'
kJPEGCodecType ='jpeg' -> JPEG, SUPPORTED
kMacPaintCodecType ='PNTG' -> Isn't this the PICT format itself? Does that make sense?! ;-)
kMacPaintCodecType ='PNTG' -> PNTG, should work, but lacks test data
kMicrosoftVideo1CodecType ='msvc'
kMotionJPEGACodecType ='mjpa'
kMotionJPEGBCodecType ='mjpb'
@@ -99,12 +99,12 @@ final class QuickTime {
kQuickDrawCodecType ='qdrw' -> QD?
kQuickDrawGXCodecType ='qdgx' -> QD?
kRawCodecType ='raw ' -> Raw (A)RGB pixel data
kSGICodecType ='.SGI'
kSGICodecType ='.SGI' -> SGI, should work, but lacks test data
k16GrayCodecType ='b16g' -> Raw 16 bit gray data?
k64ARGBCodecType ='b64a' -> Raw 64 bit (16 bpp) color data?
kSorensonCodecType ='SVQ1'
kSorensonYUV9CodecType ='syv9'
kTargaCodecType ='tga ' -> TGA, maybe create a plugin for that
kTargaCodecType ='tga ' -> TGA, should work, but lacks test data
k32AlphaGrayCodecType ='b32a' -> 16 bit gray + 16 bit alpha raw data?
kTIFFCodecType ='tiff' -> TIFF, SUPPORTED
kVectorCodecType ='path'
@@ -117,13 +117,13 @@ final class QuickTime {
/**
* Gets a decompressor that can decompress the described data.
*
* @param pDescription the image description ({@code 'idsc'} Atom).
* @param description the image description ({@code 'idsc'} Atom).
* @return a decompressor that can decompress data decribed by the given {@link ImageDesc description},
* or {@code null} if no decompressor is found
*/
private static QTDecompressor getDecompressor(final ImageDesc pDescription) {
private static QTDecompressor getDecompressor(final ImageDesc description) {
for (QTDecompressor decompressor : sDecompressors) {
if (decompressor.canDecompress(pDescription)) {
if (decompressor.canDecompress(description)) {
return decompressor;
}
}
@@ -134,13 +134,13 @@ final class QuickTime {
/**
* Decompresses the QuickTime image data from the given stream.
*
* @param pStream the image input stream
* @param stream the image input stream
* @return a {@link BufferedImage} containing the image data, or {@code null} if no decompressor is capable of
* decompressing the image.
* @throws IOException if an I/O exception occurs during read
*/
public static BufferedImage decompress(final ImageInputStream pStream) throws IOException {
ImageDesc description = ImageDesc.read(pStream);
public static BufferedImage decompress(final ImageInputStream stream) throws IOException {
ImageDesc description = ImageDesc.read(stream);
if (PICTImageReader.DEBUG) {
System.out.println(description);
@@ -152,12 +152,8 @@ final class QuickTime {
return null;
}
InputStream streamAdapter = IIOUtil.createStreamAdapter(pStream, description.dataSize);
try {
return decompressor.decompress(description, streamAdapter);
}
finally {
streamAdapter.close();
try (InputStream streamAdapter = IIOUtil.createStreamAdapter(stream, description.dataSize)) {
return decompressor.decompress(description, streamAdapter);
}
}
@@ -195,7 +191,7 @@ final class QuickTime {
byte[] extraDesc;
private ImageDesc() {}
ImageDesc() {}
public static ImageDesc read(final DataInput pStream) throws IOException {
// The following looks like the 'idsc' Atom (as described in the QuickTime File Format)

View File

@@ -0,0 +1,146 @@
package com.twelvemonkeys.imageio.plugins.pntg;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder;
import javax.imageio.IIOException;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import java.awt.*;
import java.awt.image.*;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
import static com.twelvemonkeys.imageio.plugins.pntg.PNTGImageReaderSpi.isMacBinaryPNTG;
import static com.twelvemonkeys.imageio.util.IIOUtil.subsampleRow;
/**
* PNTGImageReader.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PNTGImageReader.java,v 1.0 23/03/2021 haraldk Exp$
*/
public final class PNTGImageReader extends ImageReaderBase {
private static final Set<ImageTypeSpecifier> IMAGE_TYPES =
Collections.singleton(ImageTypeSpecifiers.createIndexed(new int[] {-1, 0}, false, -1, 1, DataBuffer.TYPE_BYTE));
protected PNTGImageReader(final ImageReaderSpi provider) {
super(provider);
}
@Override
protected void resetMembers() {
}
@Override
public int getWidth(final int imageIndex) throws IOException {
checkBounds(imageIndex);
return 576;
}
@Override
public int getHeight(final int imageIndex) throws IOException {
checkBounds(imageIndex);
return 720;
}
@Override
public Iterator<ImageTypeSpecifier> getImageTypes(final int imageIndex) throws IOException {
checkBounds(imageIndex);
return IMAGE_TYPES.iterator();
}
@Override
public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException {
checkBounds(imageIndex);
readHeader();
int width = getWidth(imageIndex);
int height = getHeight(imageIndex);
BufferedImage destination = getDestination(param, getImageTypes(imageIndex), width, height);
int[] destBands = param != null ? param.getDestinationBands() : null;
Rectangle srcRegion = new Rectangle();
Rectangle destRegion = new Rectangle();
computeRegions(param, width, height, destination, srcRegion, destRegion);
int xSub = param != null ? param.getSourceXSubsampling() : 1;
int ySub = param != null ? param.getSourceYSubsampling() : 1;
WritableRaster destRaster = destination.getRaster()
.createWritableChild(destRegion.x, destRegion.y, destRegion.width, destRegion.height, 0, 0, destBands);
Raster rowRaster = Raster.createPackedRaster(DataBuffer.TYPE_BYTE, width, 1, 1, 1, null)
.createChild(srcRegion.x, 0, destRegion.width, 1, 0, 0, destBands);
processImageStarted(imageIndex);
readData(srcRegion, destRegion, xSub, ySub, destRaster, rowRaster);
processImageComplete();
return destination;
}
private void readData(Rectangle srcRegion, Rectangle destRegion, int xSub, int ySub, WritableRaster destRaster, Raster rowRaster) throws IOException {
byte[] rowData = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
try (DataInputStream decoderStream = new DataInputStream(new DecoderStream(IIOUtil.createStreamAdapter(imageInput), new PackBitsDecoder()))) {
int srcMaxY = srcRegion.y + srcRegion.height;
for (int y = 0; y < srcMaxY; y++) {
decoderStream.readFully(rowData);
if (y >= srcRegion.y && y % ySub == 0) {
subsampleRow(rowData, srcRegion.x, srcRegion.width, rowData, destRegion.x, 1, 1, xSub);
int destY = (y - srcRegion.y) / ySub;
destRaster.setDataElements(0, destY, rowRaster);
processImageProgress(y / (float) srcMaxY);
}
if (abortRequested()) {
processReadAborted();
break;
}
}
}
}
@Override
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
checkBounds(imageIndex);
return new PNTGMetadata();
}
private void readHeader() throws IOException {
if (isMacBinaryPNTG(imageInput)) {
// Seek to end of MacBinary header
// TODO: Could actually get the file name, creation date etc metadata from this data
imageInput.seek(128);
}
else {
imageInput.seek(0);
}
// Skip pattern data section (usually all 0s)
if (imageInput.skipBytes(512) != 512) {
throw new IIOException("Could not skip pattern data");
}
}
}

View File

@@ -0,0 +1,71 @@
package com.twelvemonkeys.imageio.plugins.pntg;
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase;
import javax.imageio.stream.ImageInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.util.Locale;
/**
* PNTGImageReaderSpi.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PNTGImageReaderSpi.java,v 1.0 23/03/2021 haraldk Exp$
*/
public final class PNTGImageReaderSpi extends ImageReaderSpiBase {
public PNTGImageReaderSpi() {
super(new PNTGProviderInfo());
}
@Override
public boolean canDecodeInput(final Object source) throws IOException {
if (!(source instanceof ImageInputStream)) {
return false;
}
ImageInputStream stream = (ImageInputStream) source;
stream.mark();
try {
// TODO: Figure out how to read the files without the MacBinary header...
// Probably not possible, as it's just 512 bytes of nulls OR pattern information
return isMacBinaryPNTG(stream);
}
catch (EOFException ignore) {
return false;
}
finally {
stream.reset();
}
}
static boolean isMacBinaryPNTG(final ImageInputStream stream) throws IOException {
stream.seek(0);
if (stream.readByte() != 0) {
return false;
}
byte nameLen = stream.readByte();
if (nameLen < 0 || nameLen > 63) {
return false;
}
stream.skipBytes(63);
// Validate that type is PNTG and that next 4 bytes are all within the ASCII range, typically 'MPNT'
return stream.readInt() == ('P' << 24 | 'N' << 16 | 'T' << 8 | 'G') && (stream.readInt() & 0x80808080) == 0;
}
@Override
public PNTGImageReader createReaderInstance(final Object extension) {
return new PNTGImageReader(this);
}
@Override
public String getDescription(final Locale locale) {
return "Apple MacPaint Painting (PNTG) image reader";
}
}

View File

@@ -0,0 +1,86 @@
package com.twelvemonkeys.imageio.plugins.pntg;
import com.twelvemonkeys.imageio.AbstractMetadata;
import javax.imageio.metadata.IIOMetadataNode;
/**
* PNTGMetadata.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PNTGMetadata.java,v 1.0 23/03/2021 haraldk Exp$
*/
public class PNTGMetadata extends AbstractMetadata {
@Override
protected IIOMetadataNode getStandardChromaNode() {
IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
chroma.appendChild(csType);
csType.setAttribute("name", "GRAY");
// NOTE: Channels in chroma node reflects channels in color model (see data node, for channels in data)
IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
chroma.appendChild(numChannels);
numChannels.setAttribute("value", "1");
IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero");
chroma.appendChild(blackIsZero);
blackIsZero.setAttribute("value", "FALSE");
return chroma;
}
@Override
protected IIOMetadataNode getStandardCompressionNode() {
IIOMetadataNode compressionNode = new IIOMetadataNode("Compression");
IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName");
compressionTypeName.setAttribute("value", "PackBits"); // RLE?
compressionNode.appendChild(compressionTypeName);
compressionNode.appendChild(new IIOMetadataNode("Lossless"));
// "value" defaults to TRUE
return compressionNode;
}
@Override
protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode data = new IIOMetadataNode("Data");
// PlanarConfiguration
IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration");
planarConfiguration.setAttribute("value", "PixelInterleaved");
data.appendChild(planarConfiguration);
IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
sampleFormat.setAttribute("value", "UnsignedIntegral");
data.appendChild(sampleFormat);
IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
bitsPerSample.setAttribute("value", "1");
data.appendChild(bitsPerSample);
return data;
}
@Override
protected IIOMetadataNode getStandardDocumentNode() {
IIOMetadataNode document = new IIOMetadataNode("Document");
IIOMetadataNode formatVersion = new IIOMetadataNode("FormatVersion");
document.appendChild(formatVersion);
formatVersion.setAttribute("value", "1.0");
// TODO: We could get the file creation time from MacBinary header here...
return document;
}
@Override
protected IIOMetadataNode getStandardTextNode() {
// TODO: We could get the file name from MacBinary header here...
return super.getStandardTextNode();
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (c) 2015, 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.plugins.pntg;
import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo;
/**
* PNTGProviderInfo.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: PNTGProviderInfo.java,v 1.0 20/03/15 harald.kuhr Exp$
*/
final class PNTGProviderInfo extends ReaderWriterProviderInfo {
protected PNTGProviderInfo() {
super(
PNTGProviderInfo.class,
new String[] {"pntg", "PNTG"},
new String[] {"mac", "pic", "pntg"},
new String[] {"image/x-pntg"},
"com.twelvemonkeys.imageio.plugins.mac.MACImageReader",
new String[] {"com.twelvemonkeys.imageio.plugins.mac.MACImageReaderSpi"},
null, null,
false, null, null, null, null,
true, null, null, null, null
);
}
}

View File

@@ -1 +1,2 @@
com.twelvemonkeys.imageio.plugins.pict.PICTImageReaderSpi
com.twelvemonkeys.imageio.plugins.pntg.PNTGImageReaderSpi
com.twelvemonkeys.imageio.plugins.pict.PICTImageReaderSpi

View File

@@ -0,0 +1,30 @@
package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import static org.junit.Assert.assertTrue;
/**
* QTBMPDecompressorTest.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: QTBMPDecompressorTest.java,v 1.0 24/03/2021 haraldk Exp$
*/
public class QTBMPDecompressorTest {
@Test
public void canDecompress() {
QTDecompressor decompressor = new QTBMPDecompressor();
ImageDesc description = new ImageDesc();
description.compressorVendor = QuickTime.VENDOR_APPLE;
description.compressorIdentifer = "WRLE";
description.extraDesc = "....bmp ...something...".getBytes(StandardCharsets.UTF_8);
assertTrue(decompressor.canDecompress(description));
}
}

View File

@@ -0,0 +1,52 @@
package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
/**
* QTBMPDecompressorTest.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: QTBMPDecompressorTest.java,v 1.0 24/03/2021 haraldk Exp$
*/
public class QTGenericDecompressorTest {
private ImageDesc createDescription(final String identifer, final String name, final int depth) {
ImageDesc description = new ImageDesc();
description.compressorVendor = QuickTime.VENDOR_APPLE;
description.compressorIdentifer = identifer;
description.compressorName = name;
description.depth = (short) depth;
return description;
}
@Test
public void canDecompressJPEG() {
QTDecompressor decompressor = new QTGenericDecompressor();
assertTrue(decompressor.canDecompress(createDescription("jpeg", "Photo - JPEG", 8)));
assertTrue(decompressor.canDecompress(createDescription("jpeg", "Photo - JPEG", 24)));
}
@Test
public void canDecompressPNG() {
QTDecompressor decompressor = new QTGenericDecompressor();
assertTrue(decompressor.canDecompress(createDescription("png ", "PNG", 8)));
assertTrue(decompressor.canDecompress(createDescription("png ", "PNG", 24)));
assertTrue(decompressor.canDecompress(createDescription("png ", "PNG", 32)));
}
@Test
public void canDecompressTIFF() {
QTDecompressor decompressor = new QTGenericDecompressor();
assertTrue(decompressor.canDecompress(createDescription("tiff", "TIFF", 8)));
assertTrue(decompressor.canDecompress(createDescription("tiff", "TIFF", 24)));
assertTrue(decompressor.canDecompress(createDescription("tiff", "TIFF", 32)));
}
}

View File

@@ -0,0 +1,46 @@
package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.plugins.pict.QuickTime.ImageDesc;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
/**
* QTBMPDecompressorTest.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: QTBMPDecompressorTest.java,v 1.0 24/03/2021 haraldk Exp$
*/
public class QTRAWDecompressorTest {
private ImageDesc createDescription(int bitDepth) {
ImageDesc description = new ImageDesc();
description.compressorVendor = QuickTime.VENDOR_APPLE;
description.compressorIdentifer = "raw ";
description.depth = (short) bitDepth;
return description;
}
@Test
public void canDecompressRGB() {
QTDecompressor decompressor = new QTRAWDecompressor();
assertTrue(decompressor.canDecompress(createDescription(24)));
}
@Test
public void canDecompressRGBA() {
QTDecompressor decompressor = new QTRAWDecompressor();
assertTrue(decompressor.canDecompress(createDescription(32)));
}
@Test
public void canDecompressGray() {
QTDecompressor decompressor = new QTRAWDecompressor();
assertTrue(decompressor.canDecompress(createDescription(40)));
}
}

View File

@@ -0,0 +1,65 @@
package com.twelvemonkeys.imageio.plugins.pntg;
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* PNTGImageReaderTest.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PNTGImageReaderTest.java,v 1.0 23/03/2021 haraldk Exp$
*/
public class PNTGImageReaderTest extends ImageReaderAbstractTest<PNTGImageReader> {
@Override
protected ImageReaderSpi createProvider() {
return new PNTGImageReaderSpi();
}
@Override
protected List<TestData> getTestData() {
return Arrays.asList(new TestData(getClassLoaderResource("/mac/porsches.mac"), new Dimension(576, 720)),
new TestData(getClassLoaderResource("/mac/MARBLES.MAC"), new Dimension(576, 720)));
}
@Override
protected List<String> getFormatNames() {
return Arrays.asList("PNTG", "pntg");
}
@Override
protected List<String> getSuffixes() {
return Arrays.asList("mac", "pntg");
}
@Override
protected List<String> getMIMETypes() {
return Collections.singletonList("image/x-pntg");
}
@Override
public void testProviderCanRead() throws IOException {
// TODO: This a kind of hack...
// Currently, the provider don't claim to read the MARBLES.MAC image,
// as it lacks the MacBinary header and thus no way to identify format.
// We can still read it, so we'll include it in the other tests.
List<TestData> testData = getTestData().subList(0, 1);
for (TestData data : testData) {
ImageInputStream stream = data.getInputStream();
assertNotNull(stream);
assertTrue("Provider is expected to be able to decode data: " + data, provider.canDecodeInput(stream));
}
}
}

View File

@@ -0,0 +1,17 @@
package com.twelvemonkeys.imageio.plugins.pntg;
import org.junit.Test;
/**
* PNTGMetadataTest.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PNTGMetadataTest.java,v 1.0 23/03/2021 haraldk Exp$
*/
public class PNTGMetadataTest {
@Test
public void testCreate() {
new PNTGMetadata();
}
}

View File

@@ -353,10 +353,7 @@ public final class PNMImageReader extends ImageReaderBase {
input.readFully(rowDataByte);
// Subsample (horizontal)
if (xSub > 1) {
subsampleRow(rowDataByte, srcRegion.x, srcRegion.width, rowDataByte, 0, samplesPerPixel, bitsPerSample, xSub);
}
subsampleRow(rowDataByte, srcRegion.x, srcRegion.width, rowDataByte, 0, samplesPerPixel, bitsPerSample, xSub);
normalize(rowDataByte, 0, rowDataByte.length / xSub);
int destY = (y - srcRegion.y) / ySub;
@@ -382,10 +379,7 @@ public final class PNMImageReader extends ImageReaderBase {
readFully(input, rowDataUShort);
// Subsample (horizontal)
if (xSub > 1) {
subsampleRow(rowDataUShort, srcRegion.x, srcRegion.width, rowDataUShort, 0, samplesPerPixel, 16, xSub);
}
subsampleRow(rowDataUShort, srcRegion.x, srcRegion.width, rowDataUShort, 0, samplesPerPixel, 16, xSub);
normalize(rowDataUShort);
int destY = (y - srcRegion.y) / ySub;

View File

@@ -218,7 +218,8 @@ public final class SGIImageReader extends ImageReaderBase {
private void readRowByte(int height, Rectangle srcRegion, int[] scanlineOffsets, int[] scanlineLengths, int compression, int xSub, int ySub, int c, byte[] rowDataByte, WritableRaster destChannel, Raster srcChannel, int y) throws IOException {
// If subsampled or outside source region, skip entire row
if (y % ySub != 0 || height - 1 - y < srcRegion.y || height - 1 - y >= srcRegion.y + srcRegion.height) {
int srcY = height - 1 - y;
if (srcY % ySub != 0 || srcY < srcRegion.y || srcY >= srcRegion.y + srcRegion.height) {
if (compression == SGI.COMPRESSION_NONE) {
imageInput.skipBytes(rowDataByte.length);
}
@@ -245,16 +246,17 @@ public final class SGIImageReader extends ImageReaderBase {
}
}
normalize(rowDataByte, 9, srcRegion.width / xSub);
normalize(rowDataByte, 0, srcRegion.width / xSub);
// Flip into position (SGI images are stored bottom/up)
int dstY = (height - 1 - y - srcRegion.y) / ySub;
int dstY = (srcY - srcRegion.y) / ySub;
destChannel.setDataElements(0, dstY, srcChannel);
}
private void readRowUShort(int height, Rectangle srcRegion, int[] scanlineOffsets, int[] scanlineLengths, int compression, int xSub, int ySub, int c, short[] rowDataUShort, WritableRaster destChannel, Raster srcChannel, int y) throws IOException {
// If subsampled or outside source region, skip entire row
if (y % ySub != 0 || height - 1 - y < srcRegion.y || height - 1 - y >= srcRegion.y + srcRegion.height) {
int srcY = height - 1 - y;
if (srcY % ySub != 0 || srcY < srcRegion.y || srcY >= srcRegion.y + srcRegion.height) {
if (compression == SGI.COMPRESSION_NONE) {
imageInput.skipBytes(rowDataUShort.length * 2);
}
@@ -281,10 +283,10 @@ public final class SGIImageReader extends ImageReaderBase {
}
}
normalize(rowDataUShort, 9, srcRegion.width / xSub);
normalize(rowDataUShort, 0, srcRegion.width / xSub);
// Flip into position (SGI images are stored bottom/up)
int dstY = (height - 1 - y - srcRegion.y) / ySub;
int dstY = (srcY - srcRegion.y) / ySub;
destChannel.setDataElements(0, dstY, srcChannel);
}

View File

@@ -53,7 +53,8 @@ public class SGIImageReaderTest extends ImageReaderAbstractTest<SGIImageReader>
@Override
protected List<TestData> getTestData() {
return Collections.singletonList(
return Arrays.asList(
new TestData(getClassLoaderResource("/sgi/input.sgi"), new Dimension(70, 46)), // RLE encoded RGB
new TestData(getClassLoaderResource("/sgi/MARBLES.SGI"), new Dimension(1419, 1001)) // RLE encoded RGB
);
}

Binary file not shown.

View File

@@ -200,12 +200,12 @@ final class TGAHeader {
int components = colorMap.hasAlpha() ? 4 : 3;
byte[] cmap = new byte[rgb.length * components];
for (int i = 0; i < rgb.length; i++) {
cmap[i * components ] = (byte) ((rgb[i] >> 16) & 0xff);
cmap[i * components + 1] = (byte) ((rgb[i] >> 8) & 0xff);
cmap[i * components + 2] = (byte) ((rgb[i] ) & 0xff);
cmap[i * components ] = (byte) ((rgb[i] ) & 0xff); // B
cmap[i * components + 1] = (byte) ((rgb[i] >> 8) & 0xff); // G
cmap[i * components + 2] = (byte) ((rgb[i] >> 16) & 0xff); // R
if (components == 4) {
cmap[i * components + 3] = (byte) ((rgb[i] >>> 24) & 0xff);
cmap[i * components + 3] = (byte) ((rgb[i] >>> 24) & 0xff); // A
}
}
@@ -298,9 +298,23 @@ final class TGAHeader {
hasAlpha = false;
break;
case 24:
// BGR -> RGB
for (int i = 0; i < cmap.length; i += 3) {
byte b = cmap[i];
cmap[i ] = cmap[i + 2];
cmap[i + 2] = b;
}
hasAlpha = false;
break;
case 32:
// BGRA -> RGBA
for (int i = 0; i < cmap.length; i += 4) {
byte b = cmap[i];
cmap[i ] = cmap[i + 2];
cmap[i + 2] = b;
}
hasAlpha = true;
break;
default:

View File

@@ -224,7 +224,7 @@ final class TGAImageReader extends ImageReaderBase {
byte[] rowDataByte, WritableRaster destChannel, Raster srcChannel, int y) throws IOException {
// If subsampled or outside source region, skip entire row
if (y % ySub != 0 || height - 1 - y < srcRegion.y || height - 1 - y >= srcRegion.y + srcRegion.height) {
imageInput.skipBytes(rowDataByte.length);
input.skipBytes(rowDataByte.length);
return;
}
@@ -251,7 +251,8 @@ final class TGAImageReader extends ImageReaderBase {
destChannel.setDataElements(0, dstY, srcChannel);
break;
case TGA.ORIGIN_UPPER_LEFT:
destChannel.setDataElements(0, y, srcChannel);
dstY = y / ySub;
destChannel.setDataElements(0, dstY, srcChannel);
break;
default:
throw new IIOException("Unsupported origin: " + origin);
@@ -289,7 +290,8 @@ final class TGAImageReader extends ImageReaderBase {
destChannel.setDataElements(0, dstY, srcChannel);
break;
case TGA.ORIGIN_UPPER_LEFT:
destChannel.setDataElements(0, y, srcChannel);
dstY = y / ySub;
destChannel.setDataElements(0, dstY, srcChannel);
break;
default:
throw new IIOException("Unsupported origin: " + origin);
@@ -439,6 +441,7 @@ final class TGAImageReader extends ImageReaderBase {
WritableRaster rowRaster = rawType.createBufferedImage(width, 1).getRaster();
processThumbnailStarted(imageIndex, thumbnailIndex);
processThumbnailProgress(0f);
// Thumbnail is always stored non-compressed, no need for RLE support
imageInput.seek(extensions.getThumbnailOffset() + 2);
@@ -466,6 +469,7 @@ final class TGAImageReader extends ImageReaderBase {
}
}
processThumbnailProgress(100f);
processThumbnailComplete();
return destination;

View File

@@ -189,6 +189,7 @@ final class TGAMetadata extends AbstractMetadata {
switch (header.getPixelDepth()) {
case 8:
bitsPerSample.setAttribute("value", createListValue(1, Integer.toString(header.getPixelDepth())));
break;
case 16:
if (header.getAttributeBits() > 0 && extensions != null && extensions.hasAlpha()) {
bitsPerSample.setAttribute("value", "5, 5, 5, 1");

View File

@@ -32,11 +32,19 @@ package com.twelvemonkeys.imageio.plugins.tga;
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest;
import org.junit.Test;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertNotNull;
/**
* TGAImageReaderTest
*
@@ -104,4 +112,23 @@ public class TGAImageReaderTest extends ImageReaderAbstractTest<TGAImageReader>
"image/targa", "image/x-targa"
);
}
@Test
public void testSubsampling() throws IOException {
ImageReader reader = createReader();
ImageReadParam param = reader.getDefaultReadParam();
param.setSourceSubsampling(3, 5, 0, 0);
for (TestData testData : getTestData()) {
try (ImageInputStream input = testData.getInputStream()) {
reader.setInput(input);
assertNotNull(reader.read(0, param));
}
finally {
reader.reset();
}
}
reader.dispose();
}
}

View File

@@ -152,7 +152,7 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
static int findCompressionType(final int type, final InputStream in) throws IOException {
// Discover possible incorrect type, revert to RLE
if (type == TIFFExtension.COMPRESSION_CCITT_T4 && in.markSupported()) {
byte[] streamData = new byte[20];
byte[] streamData = new byte[32];
try {
in.mark(streamData.length);
@@ -173,8 +173,9 @@ final class CCITTFaxDecoderStream extends FilterInputStream {
if (streamData[0] != 0 || (streamData[1] >> 4 != 1 && streamData[1] != 1)) {
// Leading EOL (0b000000000001) not found, search further and try RLE if not found
int numBits = streamData.length * 8;
short b = (short) (((streamData[0] << 8) + streamData[1]) >> 4);
for (int i = 12; i < 160; i++) {
for (int i = 12; i < numBits; i++) {
b = (short) ((b << 1) + ((streamData[(i / 8)] >> (7 - (i % 8))) & 0x01));
if ((b & 0xFFF) == 1) {

View File

@@ -48,15 +48,16 @@ import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
import com.twelvemonkeys.imageio.metadata.xmp.XMPReader;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.FileUtil;
import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.xml.XMLSerializer;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
@@ -695,7 +696,7 @@ public final class TIFFImageReader extends ImageReaderBase {
}
private int getPhotometricInterpretationWithFallback() throws IIOException {
// PhotometricInterpretation is a required TAG, but as it can be guessed this does a fallback that is equal to JAI ImageIO.
// PhotometricInterpretation is a required tag, but as it can be guessed this does a fallback that is similar to JAI ImageIO.
int interpretation = getValueAsIntWithDefault(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation", -1);
if (interpretation == -1) {
int compression = getValueAsIntWithDefault(TIFF.TAG_COMPRESSION, TIFFBaseline.COMPRESSION_NONE);
@@ -712,7 +713,13 @@ public final class TIFFImageReader extends ImageReaderBase {
interpretation = TIFFBaseline.PHOTOMETRIC_PALETTE;
}
else if ((samplesPerPixel - extraSamples) == 3) {
interpretation = TIFFBaseline.PHOTOMETRIC_RGB;
if (compression == TIFFExtension.COMPRESSION_JPEG
|| compression == TIFFExtension.COMPRESSION_OLD_JPEG) {
interpretation = TIFFExtension.PHOTOMETRIC_YCBCR;
}
else {
interpretation = TIFFBaseline.PHOTOMETRIC_RGB;
}
}
else if ((samplesPerPixel - extraSamples) == 4) {
interpretation = TIFFExtension.PHOTOMETRIC_SEPARATED;
@@ -958,10 +965,9 @@ public final class TIFFImageReader extends ImageReaderBase {
int tilesAcross = (width + stripTileWidth - 1) / stripTileWidth;
int tilesDown = (height + stripTileHeight - 1) / stripTileHeight;
// TODO: Get number of extra samples not part of the rawType spec...
// TODO: If extrasamples, we might need to create a raster with more samples...
// Raw type may contain extra samples
WritableRaster rowRaster = rawType.createBufferedImage(stripTileWidth, 1).getRaster();
// WritableRaster rowRaster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, stripTileWidth, 1, 2, null).createWritableChild(0, 0, stripTileWidth, 1, 0, 0, new int[]{0});
Rectangle clip = new Rectangle(srcRegion);
int srcRow = 0;
Boolean needsCSConversion = null;
@@ -1120,6 +1126,13 @@ public final class TIFFImageReader extends ImageReaderBase {
// TODO: Cache the JPEG reader for later use? Remember to reset to avoid resource leaks
ImageReader jpegReader = createJPEGDelegate();
// TODO: Use proper inner class + add case for old JPEG
jpegReader.addIIOReadWarningListener(new IIOReadWarningListener() {
@Override
public void warningOccurred(final ImageReader source, final String warning) {
processWarningOccurred(warning);
}
});
JPEGImageReadParam jpegParam = (JPEGImageReadParam) jpegReader.getDefaultReadParam();
// JPEG_TABLES should be a full JPEG 'abbreviated table specification', containing:
@@ -1134,7 +1147,8 @@ public final class TIFFImageReader extends ImageReaderBase {
// This initializes the tables and other internal settings for the reader,
// and is actually a feature of JPEG, see abbreviated streams:
// http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#abbrev
jpegReader.getStreamMetadata();
IIOMetadata streamMetadata = jpegReader.getStreamMetadata();
new XMLSerializer(System.out, "UTF8").serialize(streamMetadata.getAsTree(streamMetadata.getNativeMetadataFormatName()), false);
}
else if (tilesDown * tilesAcross > 1) {
processWarningOccurred("Missing JPEGTables for tiled/striped TIFF with compression: 7 (JPEG)");
@@ -1809,8 +1823,6 @@ public final class TIFFImageReader extends ImageReaderBase {
out.writeByte(0); // Spectral selection end
out.writeByte(0); // Approx high & low
// System.err.println(TIFFReader.HexDump.dump(stream.toByteArray()));
//
return stream.createInputStream();
}
@@ -1871,10 +1883,8 @@ public final class TIFFImageReader extends ImageReaderBase {
}
// Subsample horizontal
if (xSub != 1) {
IIOUtil.subsampleRow(rowDataByte, srcRegion.x * numBands, colsInTile,
rowDataByte, srcRegion.x * numBands / xSub, numBands, bitsPerSample, xSub);
}
subsampleRow(rowDataByte, srcRegion.x * numBands, colsInTile,
rowDataByte, srcRegion.x * numBands / xSub, numBands, bitsPerSample, xSub);
destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel);
}
@@ -1913,10 +1923,8 @@ public final class TIFFImageReader extends ImageReaderBase {
normalizeColor(interpretation, rowDataShort);
// Subsample horizontal
if (xSub != 1) {
subsampleRow(rowDataShort, srcRegion.x * numBands, colsInTile,
rowDataShort, srcRegion.x * numBands / xSub, numBands, bitsPerSample, xSub);
}
subsampleRow(rowDataShort, srcRegion.x * numBands, colsInTile,
rowDataShort, srcRegion.x * numBands / xSub, numBands, bitsPerSample, xSub);
destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel);
// TODO: Possible speedup ~30%!:
@@ -1949,10 +1957,8 @@ public final class TIFFImageReader extends ImageReaderBase {
normalizeColor(interpretation, rowDataInt);
// Subsample horizontal
if (xSub != 1) {
subsampleRow(rowDataInt, srcRegion.x * numBands, colsInTile,
rowDataInt, srcRegion.x * numBands / xSub, numBands, bitsPerSample, xSub);
}
subsampleRow(rowDataInt, srcRegion.x * numBands, colsInTile,
rowDataInt, srcRegion.x * numBands / xSub, numBands, bitsPerSample, xSub);
destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel);
}
@@ -2581,6 +2587,7 @@ public final class TIFFImageReader extends ImageReaderBase {
public static void main(final String[] args) throws IOException {
ImageIO.setUseCache(false);
deregisterOSXTIFFImageReaderSpi();
for (final String arg : args) {
File file = new File(arg);
@@ -2591,17 +2598,22 @@ public final class TIFFImageReader extends ImageReaderBase {
continue;
}
deregisterOSXTIFFImageReaderSpi();
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
if (!readers.hasNext()) {
System.err.println("No reader for: " + file);
continue;
String suffix = FileUtil.getExtension(file.getName());
readers = ImageIO.getImageReadersBySuffix(suffix);
if (!readers.hasNext()) {
System.err.println("No reader for: " + file);
continue;
}
System.err.println("Could not determine file format, falling back to file extension: ." + suffix);
}
ImageReader reader = readers.next();
System.err.printf("Reading %s format (%s)%n", reader.getFormatName(), reader);
System.out.printf("Reading %s format (%s)%n", reader.getFormatName(), reader);
reader.addIIOReadWarningListener(new IIOReadWarningListener() {
public void warningOccurred(ImageReader source, String warning) {
@@ -2660,8 +2672,8 @@ public final class TIFFImageReader extends ImageReaderBase {
try {
long start = System.currentTimeMillis();
// int width = reader.getWidth(imageNo);
// int height = reader.getHeight(imageNo);
int width = reader.getWidth(imageNo);
int height = reader.getHeight(imageNo);
// param.setSourceRegion(new Rectangle(width / 4, height / 4, width / 2, height / 2));
// param.setSourceRegion(new Rectangle(100, 300, 400, 400));
// param.setSourceRegion(new Rectangle(95, 105, 100, 100));
@@ -2669,6 +2681,7 @@ public final class TIFFImageReader extends ImageReaderBase {
// param.setDestinationOffset(new Point(50, 150));
// param.setSourceSubsampling(2, 2, 0, 0);
// param.setSourceSubsampling(3, 3, 0, 0);
// param.setSourceSubsampling(4, 4, 0, 0);
BufferedImage image = reader.read(imageNo, param);
System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");

View File

@@ -68,14 +68,19 @@ public class CCITTFaxDecoderStreamTest {
};
// group3_2d.tif: EOL|k=1|3W|1B|2W|EOL|k=0|V|V|V|EOL|k=1|3W|1B|2W|EOL|k=0|V-1|V|V|6*F
static final byte[] DATA_G3_2D = { 0x00, 0x1C, 0x27, 0x00, 0x17, 0x00, 0x1C, 0x27, 0x00, 0x12, (byte) 0xC0 };
static final byte[] DATA_G3_2D = {0x00, 0x1C, 0x27, 0x00, 0x17, 0x00, 0x1C, 0x27, 0x00, 0x12, (byte) 0xC0};
// group3_2d_fill.tif
static final byte[] DATA_G3_2D_FILL = { 0x00, 0x01, (byte) 0xC2, 0x70, 0x01, 0x70, 0x01, (byte) 0xC2, 0x70, 0x01,
0x2C };
static final byte[] DATA_G3_2D_FILL = {0x00, 0x01, (byte) 0xC2, 0x70, 0x01, 0x70, 0x01, (byte) 0xC2,
0x70, 0x01, 0x2C};
static final byte[] DATA_G3_2D_lsb2msb = { 0x00, 0x38, (byte) 0xE4, 0x00, (byte) 0xE8, 0x00, 0x38, (byte) 0xE4,
0x00, 0x48, 0x03 };
static final byte[] DATA_G3_2D_lsb2msb = {0x00, 0x38, (byte) 0xE4, 0x00, (byte) 0xE8, 0x00, 0x38, (byte) 0xE4,
0x00, 0x48, 0x03};
static final byte[] DATA_G3_LONG = {0x00, 0x68, 0x0A, (byte) 0xC9, 0x3A, 0x3A, 0x00, 0x68,
(byte) 0x8A, (byte) 0xD8, 0x3A, 0x35, 0x00, 0x68, 0x0A, 0x06,
(byte) 0xDD, 0x3A, 0x19, 0x00, 0x68, (byte) 0x8A, (byte) 0x9E, 0x75,
0x08, 0x00, 0x68};
// group4.tif:
// Line 1: V-3, V-2, V0
@@ -189,6 +194,7 @@ public class CCITTFaxDecoderStreamTest {
assertEquals(TIFFExtension.COMPRESSION_CCITT_T4, CCITTFaxDecoderStream.findCompressionType(TIFFExtension.COMPRESSION_CCITT_T4, new ByteArrayInputStream(DATA_G3_2D)));
assertEquals(TIFFExtension.COMPRESSION_CCITT_T4, CCITTFaxDecoderStream.findCompressionType(TIFFExtension.COMPRESSION_CCITT_T4, new ByteArrayInputStream(DATA_G3_2D_FILL)));
assertEquals(TIFFExtension.COMPRESSION_CCITT_T4, CCITTFaxDecoderStream.findCompressionType(TIFFExtension.COMPRESSION_CCITT_T4, new ByteArrayInputStream(DATA_G3_2D_lsb2msb)));
assertEquals(TIFFExtension.COMPRESSION_CCITT_T4, CCITTFaxDecoderStream.findCompressionType(TIFFExtension.COMPRESSION_CCITT_T4, new ByteArrayInputStream(DATA_G3_LONG)));
// Group 4/CCITT_T6
assertEquals(TIFFExtension.COMPRESSION_CCITT_T6, CCITTFaxDecoderStream.findCompressionType(TIFFExtension.COMPRESSION_CCITT_T6, new ByteArrayInputStream(DATA_G4)));

View File

@@ -171,11 +171,9 @@ final class XWDImageReader extends ImageReaderBase {
}
}
if (xSub != 1) {
// Horizontal subsampling
int samplesPerPixel = header.numComponents();
subsampleRow(row, srcRegion.x * samplesPerPixel, srcRegion.width, row, srcRegion.x * samplesPerPixel, samplesPerPixel, header.bitsPerRGB, xSub);
}
// Horizontal subsampling
int samplesPerPixel = header.numComponents();
subsampleRow(row, srcRegion.x * samplesPerPixel, srcRegion.width, row, srcRegion.x * samplesPerPixel, samplesPerPixel, header.bitsPerRGB, xSub);
raster.setDataElements(0, (y - srcRegion.y) / ySub, rowRaster);