mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2026-03-20 00:00:03 -04:00
Merge branch 'master' into webp
This commit is contained in:
@@ -104,6 +104,6 @@
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<batik.version>1.12</batik.version>
|
||||
<batik.version>1.14</batik.version>
|
||||
</properties>
|
||||
</project>
|
||||
|
||||
@@ -655,7 +655,7 @@ public class SVGImageReader extends ImageReaderBase {
|
||||
if (allowExternalResources) {
|
||||
return super.getExternalResourceSecurity(resourceURL, docURL);
|
||||
}
|
||||
return new NoLoadExternalResourceSecurity();
|
||||
return new EmbededExternalResourceSecurity(resourceURL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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..
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
com.twelvemonkeys.imageio.stream.BufferedFileImageInputStreamSpi
|
||||
com.twelvemonkeys.imageio.stream.BufferedRAFImageInputStreamSpi
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
BIN
imageio/imageio-pcx/src/test/resources/pcx/input.pcx
Normal file
BIN
imageio/imageio-pcx/src/test/resources/pcx/input.pcx
Normal file
Binary file not shown.
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
com.twelvemonkeys.imageio.plugins.pict.PICTImageReaderSpi
|
||||
com.twelvemonkeys.imageio.plugins.pntg.PNTGImageReaderSpi
|
||||
com.twelvemonkeys.imageio.plugins.pict.PICTImageReaderSpi
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
BIN
imageio/imageio-pict/src/test/resources/mac/MARBLES.MAC
Normal file
BIN
imageio/imageio-pict/src/test/resources/mac/MARBLES.MAC
Normal file
Binary file not shown.
BIN
imageio/imageio-pict/src/test/resources/mac/porsches.mac
Normal file
BIN
imageio/imageio-pict/src/test/resources/mac/porsches.mac
Normal file
Binary file not shown.
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
BIN
imageio/imageio-sgi/src/test/resources/sgi/input.sgi
Normal file
BIN
imageio/imageio-sgi/src/test/resources/sgi/input.sgi
Normal file
Binary file not shown.
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user