From 0da007ec8ce2a4d424f2c1255b09777366f4d638 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Fri, 11 Dec 2020 18:31:04 +0100 Subject: [PATCH 01/32] Minor clean-up. --- .../twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java index 35a0f3bb..c5fd9c3a 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java @@ -57,6 +57,7 @@ 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; @@ -2581,6 +2582,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,8 +2593,6 @@ public final class TIFFImageReader extends ImageReaderBase { continue; } - deregisterOSXTIFFImageReaderSpi(); - Iterator readers = ImageIO.getImageReaders(input); if (!readers.hasNext()) { From af1a6492d400e0dfc19a419a751899850799d726 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Tue, 15 Dec 2020 22:19:04 +0100 Subject: [PATCH 02/32] #577 Fix TGA subsampling + bonus metadata fix and palette conversion. --- .../imageio/plugins/tga/TGAHeader.java | 22 ++++++++++++--- .../imageio/plugins/tga/TGAImageReader.java | 8 +++--- .../imageio/plugins/tga/TGAMetadata.java | 1 + .../plugins/tga/TGAImageReaderTest.java | 27 +++++++++++++++++++ 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAHeader.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAHeader.java index 32980eb5..8b58fb23 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAHeader.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAHeader.java @@ -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: diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReader.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReader.java index 76b0c98d..9c0671b3 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReader.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReader.java @@ -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); diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadata.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadata.java index 3932b230..241a800a 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadata.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadata.java @@ -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"); diff --git a/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReaderTest.java b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReaderTest.java index 657bc992..0b3d1f71 100755 --- a/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReaderTest.java +++ b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReaderTest.java @@ -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 "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(); + } } From 74902b3fb49fe89636b6f5a379d520232d977005 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Mon, 21 Dec 2020 17:30:34 +0100 Subject: [PATCH 03/32] StandardCharsets.US_ASCII instead of Charset.forName("ascii") --- .../twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java index 8554c630..92986ea9 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java @@ -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 { From 253f04066bbafc397dd6278d531eee9569554c26 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 23 Dec 2020 11:46:58 +0100 Subject: [PATCH 04/32] #579 More reliable CCITT compression type detection --- .../plugins/tiff/CCITTFaxDecoderStream.java | 5 +++-- .../plugins/tiff/CCITTFaxDecoderStreamTest.java | 16 +++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStream.java index 878142d1..df2a871f 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStream.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStream.java @@ -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) { diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStreamTest.java index 21946a1d..dbe4d3c0 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStreamTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxDecoderStreamTest.java @@ -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))); From 6f6e65be12cdd3c333736c90649feb05dc6d5b23 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Mon, 11 Jan 2021 21:18:11 +0100 Subject: [PATCH 05/32] Added zoom to fit option. --- .../imageio/ImageReaderBase.java | 83 +++++++++++++------ 1 file changed, 57 insertions(+), 26 deletions(-) diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java index d7c910dd..9cc26a40 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java @@ -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. * @@ -450,6 +440,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 +516,21 @@ 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('='), + KeyStroke.getKeyStroke(KeyEvent.VK_0, KeyEvent.SHIFT_DOWN_MASK | Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), + KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, 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)); @@ -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 = ((JFrame) SwingUtilities.getWindowAncestor(source)).getRootPane(); + + 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; } From 8a1a90dafd3d76fcd8e2c003b8c649d4a8bb80a6 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Mon, 11 Jan 2021 21:44:14 +0100 Subject: [PATCH 06/32] Fix some corner cases in BufferedImageInputStream. --- .../stream/BufferedImageInputStream.java | 19 +- .../stream/BufferedImageInputStreamTest.java | 254 ++++++++++++++++++ 2 files changed, 264 insertions(+), 9 deletions(-) diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStream.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStream.java index dc0144c9..dca1a5ca 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStream.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStream.java @@ -255,6 +255,7 @@ public final class BufferedImageInputStream extends ImageInputStreamImpl impleme } int val = buffer.get() & 0xff; + streamPos++; accum <<= 8; accum |= val; @@ -264,9 +265,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 +280,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 +331,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; } diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStreamTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStreamTest.java index da0acc1a..17dab84c 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStreamTest.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStreamTest.java @@ -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 From ebaa69713fdc5e4ee91eec821a2ef84c40239bbd Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Mon, 11 Jan 2021 22:07:31 +0100 Subject: [PATCH 07/32] Deprecate for BufferedImageInputStream, now using buffered streams directly in all readers. --- .../stream/BufferedFileImageInputStream.java | 252 ++++++++++++ .../BufferedFileImageInputStreamSpi.java | 97 +++++ .../stream/BufferedImageInputStream.java | 9 +- .../BufferedRAFImageInputStreamSpi.java | 97 +++++ .../stream/URLImageInputStreamSpi.java | 17 +- .../javax.imageio.spi.ImageInputStreamSpi | 2 + .../BufferedFileImageInputStreamSpiTest.java | 17 + .../BufferedFileImageInputStreamTest.java | 386 ++++++++++++++++++ .../BufferedRAFImageInputStreamSpiTest.java | 18 + .../stream/ByteArrayImageInputStreamTest.java | 4 +- .../stream/ImageInputStreamSpiTest.java | 2 +- .../imageio/plugins/iff/IFFImageReader.java | 31 +- .../imageio/plugins/jpeg/JPEGImageReader.java | 82 ++-- .../jpeg/JPEGLosslessDecoderWrapper.java | 8 +- .../plugins/jpeg/JPEGImageReaderTest.java | 2 +- .../jpeg/JPEGSegmentImageInputStreamTest.java | 2 +- .../imageio/metadata/xmp/XMPScanner.java | 18 +- .../imageio/plugins/tiff/TIFFImageReader.java | 5 +- 18 files changed, 944 insertions(+), 105 deletions(-) create mode 100644 imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStream.java create mode 100644 imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpi.java create mode 100644 imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedRAFImageInputStreamSpi.java create mode 100644 imageio/imageio-core/src/main/resources/META-INF/services/javax.imageio.spi.ImageInputStreamSpi create mode 100644 imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpiTest.java create mode 100755 imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamTest.java create mode 100644 imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedRAFImageInputStreamSpiTest.java diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStream.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStream.java new file mode 100644 index 00000000..0017b7fd --- /dev/null +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStream.java @@ -0,0 +1,252 @@ +/* + * 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 BufferedFileImageInputStream that will read from a given File. + * + * @param file a File to read from. + * @throws IllegalArgumentException if file is null. + * @throws FileNotFoundException if file is a directory or cannot be opened for reading + * for any reason. + * @throws IOException if an I/O error occurs. + */ + public BufferedFileImageInputStream(final File file) throws IOException { + this(new RandomAccessFile(notNull(file, "file"), "r")); + } + + /** + * Constructs a BufferedFileImageInputStream that will read from a given RandomAccessFile. + * + * @param raf a RandomAccessFile to read from. + * @throws IllegalArgumentException if raf is null. + */ + 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; + } +} diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpi.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpi.java new file mode 100644 index 00000000..9574ae71 --- /dev/null +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpi.java @@ -0,0 +1,97 @@ +/* + * 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.IOException; +import java.util.Iterator; +import java.util.Locale; + +/** + * BufferedFileImageInputStreamSpi + * Experimental + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: BufferedFileImageInputStreamSpi.java,v 1.0 May 15, 2008 2:14:59 PM haraldk Exp$ + */ +public class BufferedFileImageInputStreamSpi extends ImageInputStreamSpi { + public 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 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) throws IOException { + if (input instanceof File) { + File file = (File) input; + return new BufferedFileImageInputStream(file); + } + else { + throw new IllegalArgumentException("Expected input of type URL: " + 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; + } + } +} diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStream.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStream.java index dca1a5ca..9b320929 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStream.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedImageInputStream.java @@ -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. + *

+ * NOTE: Invoking {@code close()} will NOT close the wrapped stream. * * @author Harald Kuhr * @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; diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedRAFImageInputStreamSpi.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedRAFImageInputStreamSpi.java new file mode 100644 index 00000000..bb5e3fef --- /dev/null +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedRAFImageInputStreamSpi.java @@ -0,0 +1,97 @@ +/* + * 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 Harald Kuhr + * @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 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) { + RandomAccessFile file = (RandomAccessFile) input; + return new BufferedFileImageInputStream(file); + } + else { + throw new IllegalArgumentException("Expected input of type URL: " + 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; + } + } +} diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/URLImageInputStreamSpi.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/URLImageInputStreamSpi.java index e0426b0a..b56204c7 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/URLImageInputStreamSpi.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/URLImageInputStreamSpi.java @@ -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.. } } }; diff --git a/imageio/imageio-core/src/main/resources/META-INF/services/javax.imageio.spi.ImageInputStreamSpi b/imageio/imageio-core/src/main/resources/META-INF/services/javax.imageio.spi.ImageInputStreamSpi new file mode 100644 index 00000000..c31ffbaf --- /dev/null +++ b/imageio/imageio-core/src/main/resources/META-INF/services/javax.imageio.spi.ImageInputStreamSpi @@ -0,0 +1,2 @@ +com.twelvemonkeys.imageio.stream.BufferedFileImageInputStreamSpi +com.twelvemonkeys.imageio.stream.BufferedRAFImageInputStreamSpi diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpiTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpiTest.java new file mode 100644 index 00000000..5622fd2f --- /dev/null +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpiTest.java @@ -0,0 +1,17 @@ +package com.twelvemonkeys.imageio.stream; + +import javax.imageio.spi.ImageInputStreamSpi; +import java.io.File; +import java.io.IOException; + +public class BufferedFileImageInputStreamSpiTest extends ImageInputStreamSpiTest { + @Override + protected ImageInputStreamSpi createProvider() { + return new BufferedFileImageInputStreamSpi(); + } + + @Override + protected File createInput() throws IOException { + return File.createTempFile("test-", ".tst"); + } +} \ No newline at end of file diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamTest.java new file mode 100755 index 00000000..822db90e --- /dev/null +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamTest.java @@ -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 Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: BufferedFileImageInputStreamTestCase.java,v 1.0 Apr 21, 2009 10:58:48 AM haraldk Exp$ + */ +public class 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(); + } +} diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedRAFImageInputStreamSpiTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedRAFImageInputStreamSpiTest.java new file mode 100644 index 00000000..8441bcb8 --- /dev/null +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedRAFImageInputStreamSpiTest.java @@ -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 { + @Override + protected ImageInputStreamSpi createProvider() { + return new BufferedRAFImageInputStreamSpi(); + } + + @Override + protected RandomAccessFile createInput() throws IOException { + return new RandomAccessFile(File.createTempFile("test-", ".tst"), "r"); + } +} \ No newline at end of file diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStreamTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStreamTest.java index 35a3d677..5d770994 100755 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStreamTest.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/ByteArrayImageInputStreamTest.java @@ -39,11 +39,11 @@ import static com.twelvemonkeys.imageio.stream.BufferedImageInputStreamTest.rang import static org.junit.Assert.*; /** - * ByteArrayImageInputStreamTestCase + * ByteArrayImageInputStreamTest * * @author Harald Kuhr * @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); diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/ImageInputStreamSpiTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/ImageInputStreamSpiTest.java index df95fee9..cbc69648 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/ImageInputStreamSpiTest.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/ImageInputStreamSpiTest.java @@ -18,7 +18,7 @@ abstract class ImageInputStreamSpiTest { protected abstract ImageInputStreamSpi createProvider(); - protected abstract T createInput(); + protected abstract T createInput() throws IOException; @Test public void testInputClass() { diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java index cc2f1439..d3c8f6ef 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java @@ -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) { diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index fc89031f..84da94f2 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -30,37 +30,6 @@ 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 javax.imageio.IIOException; -import javax.imageio.ImageIO; -import javax.imageio.ImageReadParam; -import javax.imageio.ImageReader; -import javax.imageio.ImageTypeSpecifier; -import javax.imageio.event.IIOReadUpdateListener; -import javax.imageio.event.IIOReadWarningListener; -import javax.imageio.metadata.IIOMetadata; -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; @@ -72,7 +41,6 @@ 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; @@ -80,6 +48,23 @@ import com.twelvemonkeys.imageio.util.ProgressListenerBase; import com.twelvemonkeys.lang.Validate; import com.twelvemonkeys.xml.XMLSerializer; +import javax.imageio.*; +import javax.imageio.event.IIOReadUpdateListener; +import javax.imageio.event.IIOReadWarningListener; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; +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}, * that adds support and properly handles cases where the JRE version throws exceptions. @@ -140,7 +125,7 @@ public final class JPEGImageReader extends ImageReaderBase { private List segments; private int currentStreamIndex = 0; - private List streamOffsets = new ArrayList<>(); + private final List streamOffsets = new ArrayList<>(); protected JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) { super(provider); @@ -197,10 +182,10 @@ public final class JPEGImageReader extends ImageReaderBase { return true; } } - catch (IIOException ignore) { + catch (IIOException e) { // May happen if no SOF is found, in case we'll just fall through if (DEBUG) { - ignore.printStackTrace(); + e.printStackTrace(); } } @@ -747,26 +732,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.>emptyMap()); + JPEGSegmentUtil.readSegments(imageInput, Collections.>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 +765,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 +827,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 { @@ -1392,7 +1376,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 +1395,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 { diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGLosslessDecoderWrapper.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGLosslessDecoderWrapper.java index 07d7d7dd..fdac72db 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGLosslessDecoderWrapper.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGLosslessDecoderWrapper.java @@ -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 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 segments, final ImageInputStream input) throws IOException { // TODO: Can perhaps be implemented faster return readImage(segments, input).getRaster(); diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java index b5a95d7d..64b0d300 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java @@ -1415,7 +1415,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest Date: Mon, 11 Jan 2021 22:10:25 +0100 Subject: [PATCH 08/32] #582: Fix for missing Exif thumbnail, now only issues warning. --- .../imageio/plugins/jpeg/JPEGImageReader.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 84da94f2..0b25a3f0 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -1113,13 +1113,13 @@ public final class JPEGImageReader extends ImageReaderBase { Application exif = exifSegments.get(0); // Identifier is "Exif\0" + 1 byte pad - int offset = exif.identifier.length() + 2; + int dataOffset = exif.identifier.length() + 2; - if (exif.data.length <= offset) { + if (exif.data.length <= dataOffset) { processWarningOccurred("Exif chunk has no data."); } else { - ImageInputStream stream = new ByteArrayImageInputStream(exif.data, offset, exif.data.length - offset); + ImageInputStream stream = new ByteArrayImageInputStream(exif.data, dataOffset, exif.data.length - dataOffset); CompoundDirectory exifMetadata = (CompoundDirectory) new TIFFReader().read(stream); if (exifMetadata.directoryCount() == 2) { @@ -1130,14 +1130,18 @@ public final class JPEGImageReader extends ImageReaderBase { 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); + Entry jpegOffEntry = ifd1.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT); + if (jpegOffEntry != null) { + Entry jpegLenEntry = ifd1.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); - if ((jpegLength == null || ((Number) jpegLength.getValue()).longValue() > 0)) { + // 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 <= stream.length()) { thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), 0, thumbnails.size(), ifd1, stream)); } else { - processWarningOccurred("EXIF IFD with empty (zero-length) thumbnail"); + processWarningOccurred("EXIF IFD with empty or incomplete JPEG thumbnail"); } } else { From fb1937ae6384e16d629cd217a24b31ab2247a3de Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sat, 23 Jan 2021 17:53:28 +0100 Subject: [PATCH 09/32] Updated README with latest version numbers. --- README.md | 65 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 21e19097..09513d71 100644 --- a/README.md +++ b/README.md @@ -268,12 +268,12 @@ To depend on the JPEG and TIFF plugin using Maven, add the following to your POM com.twelvemonkeys.imageio imageio-jpeg - 3.6.1 + 3.6.2 com.twelvemonkeys.imageio imageio-tiff - 3.6.1 + 3.6.2 com.twelvemonkeys.servlet @@ -391,7 +391,6 @@ ImageIO plugins * [imageio-tga-3.6.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-tga/3.6.2/imageio-tga-3.6.2.jar) * [imageio-thumbsdb-3.6.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-thumbsdb/3.6.2/imageio-thumbsdb-3.6.2.jar) * [imageio-tiff-3.6.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-tiff/3.6.2/imageio-tiff-3.6.2.jar) -* [imageio-xwd-3.6.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-xwd/3.6.2/imageio-xwd-3.6.2.jar) ImageIO plugins requiring 3rd party libs * [imageio-batik-3.6.2.jar](http://search.maven.org/remotecontent?filepath=com/twelvemonkeys/imageio/imageio-batik/3.6.2/imageio-batik-3.6.2.jar) From 88bd9cd2ba91f54f46de45f1047f7f11c973395f Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 4 Feb 2021 19:47:45 +0100 Subject: [PATCH 11/32] Update README.md Removed JDK 7 from recommended build. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c1375ceb..8539dfe3 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ Build the project (using [Maven](http://maven.apache.org/download.cgi)): $ mvn package -Currently, the recommended JDK for making a build is Oracle JDK 7.x or 8.x. +Currently, the recommended JDK for making a build is Oracle JDK 8.x. It's possible to build using OpenJDK, but some tests might fail due to some minor differences between the color management systems used. You will need to either disable the tests in question, or build without tests altogether. From 72cd3aade3e2fbf06bf999751ef6f955f49d051d Mon Sep 17 00:00:00 2001 From: Koen De Groote Date: Wed, 24 Feb 2021 14:54:46 +0100 Subject: [PATCH 12/32] Upgraded the Apache Batik library from 1.12 to 1.14 due to fixed CVEs. --- imageio/imageio-batik/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imageio/imageio-batik/pom.xml b/imageio/imageio-batik/pom.xml index f2cdb306..48d90318 100644 --- a/imageio/imageio-batik/pom.xml +++ b/imageio/imageio-batik/pom.xml @@ -104,6 +104,6 @@ - 1.12 + 1.14 From 3e3acf333295d05dbcb55e584836caa57efbf78c Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Fri, 29 Jan 2021 14:53:37 +0100 Subject: [PATCH 13/32] More standard key mapping, more correct fit size. Nicer color! --- .../java/com/twelvemonkeys/imageio/ImageReaderBase.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java index 9cc26a40..5055bc64 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java @@ -528,9 +528,8 @@ public abstract class ImageReaderBase extends ImageReader { KeyStroke.getKeyStroke('0'), KeyStroke.getKeyStroke(KeyEvent.VK_0, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); bindAction(new ZoomToFitAction("Zoom fit"), ZOOM_FIT, - KeyStroke.getKeyStroke('='), - KeyStroke.getKeyStroke(KeyEvent.VK_0, KeyEvent.SHIFT_DOWN_MASK | Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), - KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + 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())); @@ -568,7 +567,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); @@ -703,7 +702,7 @@ public abstract class ImageReaderBase extends ImageReader { source = (JComponent) menu.getInvoker(); } - Container container = ((JFrame) SwingUtilities.getWindowAncestor(source)).getRootPane(); + Container container = SwingUtilities.getAncestorOfClass(JViewport.class, source); double ratioX = container.getWidth() / (double) image.getWidth(); double ratioY = container.getHeight() / (double) image.getHeight(); From fbc738f2d4836e82315831774c0e722a4984e967 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sat, 30 Jan 2021 16:50:54 +0100 Subject: [PATCH 14/32] JPEG Exif/thumbnail fixes. --- .../imageio/plugins/jpeg/JPEGImageReader.java | 83 ++++++++++++------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 0b25a3f0..f7e80c1f 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -903,6 +903,9 @@ public final class JPEGImageReader extends ImageReaderBase { try (ImageInputStream stream = new ByteArrayImageInputStream(exif.data, offset, exif.data.length - offset)) { return (CompoundDirectory) new TIFFReader().read(stream); } + catch (IIOException e) { + processWarningOccurred("Exif chunk is present, but can't be read: " + e.getMessage()); + } } } @@ -1090,6 +1093,7 @@ public final class JPEGImageReader extends ImageReaderBase { // Read JFIF thumbnails if present JFIF jfif = getJFIF(); if (jfif != null && jfif.thumbnail != null) { + // TODO: Check if the JFIF segment really has room for this thumbnail? thumbnails.add(new JFIFThumbnailReader(thumbnailProgressDelegator, imageIndex, thumbnails.size(), jfif)); } @@ -1100,6 +1104,7 @@ public final class JPEGImageReader extends ImageReaderBase { case JFXX.JPEG: case JFXX.INDEXED: case JFXX.RGB: + // TODO: Check if the JFXX segment really has room for this thumbnail? thumbnails.add(new JFXXThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), imageIndex, thumbnails.size(), jfxx)); break; default: @@ -1120,45 +1125,67 @@ public final class JPEGImageReader extends ImageReaderBase { } else { ImageInputStream stream = new ByteArrayImageInputStream(exif.data, dataOffset, exif.data.length - dataOffset); - CompoundDirectory exifMetadata = (CompoundDirectory) new TIFFReader().read(stream); + try { + CompoundDirectory exifMetadata = (CompoundDirectory) new TIFFReader().read(stream); - if (exifMetadata.directoryCount() == 2) { - Directory ifd1 = exifMetadata.getDirectory(1); + 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(); + // 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) { - Entry jpegOffEntry = ifd1.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT); - if (jpegOffEntry != null) { - Entry jpegLenEntry = ifd1.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); + if (compression == 6) { + 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 <= stream.length()) { - thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), 0, thumbnails.size(), ifd1, stream)); + // 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 <= stream.length()) { + // Verify first bytes are FFD8 + stream.seek(jpegOffset); + if (stream.readUnsignedShort() == JPEG.SOI) { + thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), 0, thumbnails.size(), ifd1, stream)); + } + // TODO: Simplify this warning fallback stuff... + else { + processWarningOccurred("EXIF IFD with empty or incomplete JPEG thumbnail"); + } + } + else { + processWarningOccurred("EXIF IFD with empty or incomplete JPEG thumbnail"); + } } else { - processWarningOccurred("EXIF IFD with empty or incomplete JPEG thumbnail"); + processWarningOccurred("EXIF IFD with JPEG thumbnail missing JPEGInterchangeFormat tag"); + } + } + else if (compression == 1) { + Entry stripOffEntry = ifd1.getEntryById(TIFF.TAG_STRIP_OFFSETS); + if (stripOffEntry != null) { + long stripOffset = ((Number) stripOffEntry.getValue()).longValue(); + + if (stripOffset < stream.length()) { + // TODO: Verify length of Exif thumbnail vs length of segment like in JPEG + // ...but this requires so many extra values... Instead move this logic to the + // EXIFThumbnailReader? + 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 JPEG thumbnail missing JPEGInterchangeFormat tag"); + processWarningOccurred("EXIF IFD with unknown compression (expected 1 or 6): " + compression); } } - 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); - } + } + catch (IIOException e) { + processWarningOccurred("Exif chunk present, but can't be read: " + e.getMessage()); } } } From 80c595cea83dd088946f7518ccbddbfb0e7b10b3 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sat, 30 Jan 2021 16:51:25 +0100 Subject: [PATCH 15/32] No longer reads thumbnails, as part of the readWithOrientation method. --- .../contrib/exif/EXIFUtilities.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/contrib/src/main/java/com/twelvemonkeys/contrib/exif/EXIFUtilities.java b/contrib/src/main/java/com/twelvemonkeys/contrib/exif/EXIFUtilities.java index e72e7ba5..5a23a43b 100644 --- a/contrib/src/main/java/com/twelvemonkeys/contrib/exif/EXIFUtilities.java +++ b/contrib/src/main/java/com/twelvemonkeys/contrib/exif/EXIFUtilities.java @@ -2,6 +2,7 @@ package com.twelvemonkeys.contrib.exif; import com.twelvemonkeys.image.ImageUtil; import com.twelvemonkeys.imageio.ImageReaderBase; + import org.w3c.dom.NodeList; import javax.imageio.IIOImage; @@ -82,12 +83,11 @@ public class EXIFUtilities { ImageReader reader = readers.next(); try { reader.setInput(input, true, false); - IIOImage image = reader.readAll(0, reader.getDefaultReadParam()); - BufferedImage bufferedImage = ImageUtil.toBuffered(image.getRenderedImage()); - image.setRenderedImage(applyOrientation(bufferedImage, findImageOrientation(image.getMetadata()).value())); + IIOMetadata metadata = reader.getImageMetadata(0); + BufferedImage bufferedImage = applyOrientation(reader.read(0), findImageOrientation(metadata).value()); - return image; + return new IIOImage(bufferedImage, null, metadata); } finally { reader.dispose(); @@ -123,9 +123,15 @@ public class EXIFUtilities { for (String arg : args) { File input = new File(arg); - // Read everything (similar to ImageReader.readAll(0, null)), but applies the correct image orientation + // Read everything but thumbnails (similar to ImageReader.readAll(0, null)), + // and applies the correct image orientation IIOImage image = readWithOrientation(input); + if (image == null) { + System.err.printf("No reader for %s%n", input); + continue; + } + // Finds the orientation as defined by the javax_imageio_1.0 format Orientation orientation = findImageOrientation(image.getMetadata()); From ea74ac271453524b395b19f2aee406550db13778 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Sat, 30 Jan 2021 17:09:00 +0100 Subject: [PATCH 16/32] JPEG Exif/thumbnail fixes pt II. --- .../imageio/plugins/jpeg/JPEGImageReader.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index f7e80c1f..af312269 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -62,6 +62,7 @@ import java.awt.color.ICC_ColorSpace; import java.awt.color.ICC_Profile; import java.awt.image.*; import java.io.*; +import java.nio.ByteOrder; import java.util.List; import java.util.*; @@ -1146,6 +1147,7 @@ public final class JPEGImageReader extends ImageReaderBase { if (jpegLength > 0 && jpegOffset + jpegLength <= stream.length()) { // Verify first bytes are FFD8 stream.seek(jpegOffset); + stream.setByteOrder(ByteOrder.BIG_ENDIAN); if (stream.readUnsignedShort() == JPEG.SOI) { thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), 0, thumbnails.size(), ifd1, stream)); } @@ -1173,7 +1175,9 @@ public final class JPEGImageReader extends ImageReaderBase { // EXIFThumbnailReader? thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), 0, thumbnails.size(), ifd1, stream)); } - + else { + processWarningOccurred("EXIF IFD with empty or incomplete uncompressed thumbnail"); + } } else { processWarningOccurred("EXIF IFD with uncompressed thumbnail missing StripOffsets tag"); @@ -1230,7 +1234,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 From f5959af2e154134cd7767c43e139578f79b0618f Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Tue, 2 Feb 2021 14:58:39 +0100 Subject: [PATCH 17/32] New stream SPIs now behave more like the built-in SPIs. --- .../stream/BufferedFileImageInputStream.java | 3 +-- .../BufferedFileImageInputStreamSpi.java | 21 ++++++++++++------- .../BufferedRAFImageInputStreamSpi.java | 8 +++---- .../BufferedFileImageInputStreamSpiTest.java | 13 ++++++++++++ .../stream/ImageInputStreamSpiTest.java | 4 ++-- 5 files changed, 33 insertions(+), 16 deletions(-) diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStream.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStream.java index 0017b7fd..50106903 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStream.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStream.java @@ -76,9 +76,8 @@ public final class BufferedFileImageInputStream extends ImageInputStreamImpl { * @throws IllegalArgumentException if file is null. * @throws FileNotFoundException if file is a directory or cannot be opened for reading * for any reason. - * @throws IOException if an I/O error occurs. */ - public BufferedFileImageInputStream(final File file) throws IOException { + public BufferedFileImageInputStream(final File file) throws FileNotFoundException { this(new RandomAccessFile(notNull(file, "file"), "r")); } diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpi.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpi.java index 9574ae71..edc3b169 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpi.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpi.java @@ -36,7 +36,7 @@ import javax.imageio.spi.ImageInputStreamSpi; import javax.imageio.spi.ServiceRegistry; import javax.imageio.stream.ImageInputStream; import java.io.File; -import java.io.IOException; +import java.io.FileNotFoundException; import java.util.Iterator; import java.util.Locale; @@ -69,14 +69,21 @@ public class BufferedFileImageInputStreamSpi extends ImageInputStreamSpi { } } - public ImageInputStream createInputStreamInstance(final Object input, final boolean pUseCache, final File pCacheDir) throws IOException { + public ImageInputStream createInputStreamInstance(final Object input, final boolean pUseCache, final File pCacheDir) { if (input instanceof File) { - File file = (File) input; - return new BufferedFileImageInputStream(file); - } - else { - throw new IllegalArgumentException("Expected input of type URL: " + input); + 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 diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedRAFImageInputStreamSpi.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedRAFImageInputStreamSpi.java index bb5e3fef..69bac835 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedRAFImageInputStreamSpi.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/stream/BufferedRAFImageInputStreamSpi.java @@ -71,12 +71,10 @@ public class BufferedRAFImageInputStreamSpi extends ImageInputStreamSpi { public ImageInputStream createInputStreamInstance(final Object input, final boolean pUseCache, final File pCacheDir) { if (input instanceof RandomAccessFile) { - RandomAccessFile file = (RandomAccessFile) input; - return new BufferedFileImageInputStream(file); - } - else { - throw new IllegalArgumentException("Expected input of type URL: " + input); + return new BufferedFileImageInputStream((RandomAccessFile) input); } + + throw new IllegalArgumentException("Expected input of type RandomAccessFile: " + input); } @Override diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpiTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpiTest.java index 5622fd2f..9c2a2b18 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpiTest.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/BufferedFileImageInputStreamSpiTest.java @@ -1,9 +1,14 @@ 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 { @Override protected ImageInputStreamSpi createProvider() { @@ -14,4 +19,12 @@ public class BufferedFileImageInputStreamSpiTest extends ImageInputStreamSpiTest 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)); + } } \ No newline at end of file diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/ImageInputStreamSpiTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/ImageInputStreamSpiTest.java index cbc69648..00e9841c 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/ImageInputStreamSpiTest.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/stream/ImageInputStreamSpiTest.java @@ -11,10 +11,10 @@ import java.util.Locale; import static org.junit.Assert.*; abstract class ImageInputStreamSpiTest { - private final ImageInputStreamSpi provider = createProvider(); + protected final ImageInputStreamSpi provider = createProvider(); @SuppressWarnings("unchecked") - private final Class inputClass = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + protected final Class inputClass = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; protected abstract ImageInputStreamSpi createProvider(); From 6d192968d168c867c417394416361e0b2e44e4b2 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Tue, 16 Feb 2021 20:44:52 +0100 Subject: [PATCH 18/32] Fix SGI source subsampling + test optimizations. --- .../imageio/util/ImageReaderAbstractTest.java | 9 ++++----- .../imageio/plugins/pcx/PCXImageReaderTest.java | 3 ++- .../src/test/resources/pcx/input.pcx | Bin 0 -> 11613 bytes .../imageio/plugins/sgi/SGIImageReader.java | 14 ++++++++------ .../imageio/plugins/sgi/SGIImageReaderTest.java | 3 ++- .../src/test/resources/sgi/input.sgi | Bin 0 -> 11683 bytes 6 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 imageio/imageio-pcx/src/test/resources/pcx/input.pcx create mode 100644 imageio/imageio-sgi/src/test/resources/sgi/input.sgi diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java index ea4b6ec2..92bc7025 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java @@ -553,10 +553,10 @@ public abstract class ImageReaderAbstractTest { 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 +566,6 @@ public abstract class ImageReaderAbstractTest { 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)); } } diff --git a/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReaderTest.java b/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReaderTest.java index afddeedf..0a99f961 100755 --- a/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReaderTest.java +++ b/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReaderTest.java @@ -64,7 +64,7 @@ public class PCXImageReaderTest extends ImageReaderAbstractTest @Override protected List 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 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) diff --git a/imageio/imageio-pcx/src/test/resources/pcx/input.pcx b/imageio/imageio-pcx/src/test/resources/pcx/input.pcx new file mode 100644 index 0000000000000000000000000000000000000000..c9c0092b44f8d77fe702cce4e32bd07cd1496130 GIT binary patch literal 11613 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 srcRegion.y + srcRegion.height) { + int destY = height - 1 - y; + if (destY % ySub != 0 || destY < srcRegion.y || destY >= 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 = (destY - 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 destY = height - 1 - y; + if (destY % ySub != 0 || destY < srcRegion.y || destY >= 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 = (destY - srcRegion.y) / ySub; destChannel.setDataElements(0, dstY, srcChannel); } diff --git a/imageio/imageio-sgi/src/test/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReaderTest.java b/imageio/imageio-sgi/src/test/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReaderTest.java index aea3f3b4..df3132b2 100755 --- a/imageio/imageio-sgi/src/test/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReaderTest.java +++ b/imageio/imageio-sgi/src/test/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReaderTest.java @@ -53,7 +53,8 @@ public class SGIImageReaderTest extends ImageReaderAbstractTest @Override protected List 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 ); } diff --git a/imageio/imageio-sgi/src/test/resources/sgi/input.sgi b/imageio/imageio-sgi/src/test/resources/sgi/input.sgi new file mode 100644 index 0000000000000000000000000000000000000000..6a65180810a04c2291576bb299b0a238662bc682 GIT binary patch literal 11683 zcmeHtXLMB8mgc>$gkMj`H`8Hyru#iV3`mvZy;V0>&OxP8D&0r_5T@Lhq`rBeS4pMcKG%|dr=hOAZA1i z|Ni@bf&cjY{~!NPXyB^|1o=G=2F&|k^dQK8+lnB6*@z&26^|f)Erv0SAb)cWLH>3- zg8bbq1o=ZDg8X*_g8Yw>FoqE1@8Q}%>_w1&oR1*?G!8-jXC#9Bvj{={*I&VSfFS?- zAcFkMTm<=7cyaC5gtd7uNDI4BEW0{%zuLM0dSuI-0;j_1CGD30_HI= z0OQ~F0`6^qdm3QQ2h0}04A=e||GFD+&jH+k z|7$y7<^pE8Hu5&$J_NXz0Pae_404A`0W;tadjhym0q(Vcy9qES0%iq*i2eY$;rD*O z3vf>d%-MjM1ApAz=PX82148QNRsyhgJb* zFJQ(2Gw7K23UHqW+^YchM8KT@F9UrdCK%@bI0k%3a0Jf>&)I)7V4i=y_qXT%Z^vKW z75w}^-530w;I%LJ1hE9a2Y>57KlA0@;Q8Qja0KrQjv&_Hv%z76@ms9H<1fz!kH5V4 zx5r{pJ0^@4vnG%k%%0&mf-PJ{vp^j{p4b;P)^3{?GRZf9JR7e*3#$&VRxG zMUTPrU#|W3{{QN6@YyfeZS#|S*|`M;`8k<2C*OT|e&f`sN5exy!y^LghX>ye4-W+b zj|wAWq9UADlQTIj)*It8Fa$x86wS~iuC!PvEowzEWKMQ&eoj_)R(8RZ3j+hsdk_88 zKM*q9Iy5vmH2m@DKuBQ4{cMNZ9S(m{nVGTCkv6?XXVB^N29rt48dK9W5ziShBxR|j5RKfZa_{QzgUWAO8*!J)yo4?hnD2A`jGh~x@PiHoILGs}?55PX_S zsUjE(S4wb(pimtuL+1Fib8>U?iwgYyqr-u~>#pN31HT`h{Q1*RNMPm1dtEtk$w{#u zSBjRkQAURo#L9M>?Ah^VjaseI>eO16R>~wIh23dG9Vm@7M8$BtGh>qyB8#sE z24DB@+WtH+I1n<}7#MsvzzNJ6zI$>lEtF%Fif~Bz0=toe&%|(oCMiNG7mCF^xk82# zs2FL;&f(-v%PT6#%Gvih5O{f`_tCF_21W$t^e(T=%*-pu&B^e^XPQV;Ns^qSnn4gM zrCcoJ@`VzXmFZCvh}GcFNZD2X)0#B#hdK(46bvw@-4w~ij}8wiXT+z4hf z9C)%RS@sQ2ieYjUj$0&Qf=EjkuD}$y5|@btzXBq-3~EHB2q$M|Zf;&dQBDEO)xe_% zk8ix-z_c$6d>RNm+|e+2WJowyYU3~>hQVYb#Dh!XRK3KdBcN=zY_ z3rJRzB#?+DBB4km;0XjmA&v>LXw;30kP$ftv-69xCtiie@7;NMamU%$pc_#7=MyV` z+_kW-q_ng+FQ>FHFFQLc!&m3wOJ!1tNGuWxc_O)7X|$%G38)bHD)T45KWEbIKw$9U z<5w59{`~7(K+P*nBtvQST8)OGl(>pio9(W;c$rG2RLB)_5QQ zB*sP5K7={%yL@%`wa^+gaRQ))WjFl zY$O^5MmQp8TW)?)PSJ9J^YLS#x1{(aNC(!qe{1K7buE(`CQcY%22y1OJxg{&5l;p} zaU@g3V!1+V)4MEjzv}p_%$feotb(-C z9go(du}FQ?h{&H}Vxm0$y@P|#pFKO|Wv1Wz9Qd$ICuLYI4fZOQNaY|BOpsJ6mg^P5 zpyl!TLb-f2mB%u)84U-&Ga{!Y4`eDRI1m!p@wmgmx|h8k=sA44yQjPN$n5&sy6T$h zs*?QNoSgjJwD`hui&zE<6DdiVP+OJaiA@5LJjm}en*9Fsyrb}rH*4KW>!O$arL0z~ z)@u!5GZaZO3{9wljz%hAqqSg=5Fw0KrO3XdxwxznZ9wyoucB5)$HaItkAd~S__V{ zOGUV@tSCCchx$-2GC4gxBP1QBDm}Tr>vQ17EW1V})zrn2TD{I-((C_SFb=hZP=Z{T zLQKRYxwT+`YKK~mx#le#UsDf)RUn;_kx}p$6&)QJRns*TxKtw-$OY=U7|Lh_E2apc zP#_S2?I~cg1QH>Cw8Z09%Y;&aj1Wr%lK5zi$%)!fE7F;hn->%gq$|uSTe)MRO2KGS zR$sb$qpz>`QumdujkQfp^>wwi)s+?H<4TM2Gt1`9s`HuTw4TH;m2+%$YC;;ChQ=Y2 z)6)GT(r0F5q$QLr-%_n+SXyt@sX0cDxycBwoE$+dC4)g+2|8yZ<9$g{8k1cIOU^rQ z_2k+nv)bh&2{A5`pfMtL+=Q_(X_;se(v+H(o|eW*?|^Ab^QFaW zT_(NTY0#RCT7yxi*O4qk&@|^);V2Tkvs@ePiODP~^chH&CjIkQEox{%r=gROg`UXh z;H*VPMMuU&Mi|C021B@0PtqLIdIN+yi6j_bL}G~yoCgTX4^ui_F1OuCafJ#gPaU0< z1kyP{y5`*6oPzwqkb-@Mg*mY%qVdp`4}D!1uk_vRfAH+y&)-f7PFYn|b@Pn5ZIkM% zD@)3o+7`Ea-Khp8M0?+al8U?}G#gDr8dFn!8JzT`u+-8s(js&^hdMkr++ecWEKZwF zuVr-%6++ij9AXj05DF#6dZImvahc^_EkkN6JC?1R*N9F>n~+YACn7q^8{>|MjfwVX zWno&ayU@;P3}%PJYS6J#xl92*J{YGU2n)DEo=j_TMQK^Hh%c9jbRJK9lm)e;HiVPg zke4?ie_ugB^Y6brfAZXg?sMDrTzlNLe0pPLQ`@xpEB7ASwPI#tU3FRc>}iXZ z&dRc+IKTnqR+NmLnDx7Kgp=Bsmge(?q)kqbHFFHx6O!#T$*^jz+F-SrShGP#F)TsR z8b+yt6obUA?x@(}^4NS2P3R|gEd6f%baV>Zif}xKBRvVe*yy;(a3g_(2Z?t{1yY%u zQYdsLokR{vg;2;BOJxw9g?u5OOY6hEJ{#u2M@b~ki13KiaFEJ{)I$n2IVV4lle;e` z)v47QVs^badGtb8*MfF+z5C+RCpS-TTiD(>v7&A6tod!aFqMN< z5jo|hC6yDwF{UE*J`m2Ao}S7{Uy)!pS?#VkBTOo!ku+qGv{tR8NVSepGOWg^;}G*0 zTA_v&4)h?RSHgqiMShWsq2jS`?IG#f=YmjF<%t{E_wA(Bk!{c#zl0SXT$4#^h z7x4vQJY1zDNd-TQ$NffO36F9K#xPp0MC6Ewu*ar@7jYu{1ZkPn;fZqFV$gWhj?~9+Vh<&yrWaO}Bzu#yit<{&-+bm) z-{p&!_N+Z}`C|8#2hZ;v+x=tDvxoQZ-GBJt%-TiM8h<{pY*K1Rxto^LS;cvUd8Lpx zlp>ujS9nCEH$EfJ?~RE~@c9!WDGV|Ul45X@vFg=~l4ePoQG$rUnWQ1ti;s@an6PVM zGOIAuEtoxL?lN=(`YqC_)@WFR-jQEZm>6j>TitGp5RySYPbm0Cgav~mLCEp)25Ry8zYDVXb$!wfeS5a44RFIQbxBAMR+m}yY z-n;YigYElH^xS#;=vh&jN3yRS~v>e%@ zW?7RfI-|b5s;sKm7oU{kbKp{p0Etwrjif;36k$y;1brl-RB1T$Y^5#6l}x&qu3i#t zt)1K6K5IU@2wjbIN-;T~(q-@N-m-dCo!8 zS|teMjxy%NxxeB0a`PCW!EACwcu*IlG@X!xQO>xc*>ma}r%kUfNXg1;z5nF;wTrh- z{&=kSNayMES0222`t-%)TQ_fmjb1;rZ{H8Q0z+pUB75_?8IIHfe`bCznuV4i>*Qh? z>y9m$F|)35T2n<@a&l69BrAnf5oZ}AL6Ml8CM2dcer;GATO&1CSfg3!=l5>N6t+?70xq3j+Ogkzx2 zDyPZp+f1C%vaHTszVhX*EP-1yxg?#si0!TeE@l;vf~g^*zerOQU31x{_Wl%`4`0djs&zU!4 zY9l%sZ9_Um7|j;UnzH)bjjpp7PJW-~G{)xFRf$Hy)Nw^zNVsrun2J?XB&)I{88SJs&;s+E;}$67NpZ?PfE?KDof6t+VS1;W%Ei>YW6%0yg2pK=}YIY zUq5!_*x{=eetz;1a`-nl`yW5Qd+qwY7tby~3k1%RI+e%o%P7dn$N=fmkxsqI73CXO z;>&IASg~Y&TXt+zeC4DRB~DW+l3=u~D#D?ZaKpYHC8ija0#lJn9LHiK2raHB5-J

i?z}p)~PITS5 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 Fe*;d%;;sMy literal 0 HcmV?d00001 From 970f4f3a7ec9e6a2e046c9e8119d304a83505b9b Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Tue, 16 Feb 2021 20:51:08 +0100 Subject: [PATCH 19/32] #588 Clipping path from JPEG with multiple APP13 segments --- .../com/twelvemonkeys/imageio/path/Paths.java | 18 +++++++++++++----- .../imageio/metadata/jpeg/JPEGSegmentUtil.java | 2 ++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java index f8db298b..ba4a3adf 100755 --- a/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java +++ b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java @@ -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 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); diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java index 92986ea9..a788d3d0 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java @@ -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 { From 97a8806bfb0c0bab47df74a53267dee4e32a395b Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 17 Feb 2021 09:16:58 +0100 Subject: [PATCH 20/32] Better name for source y... --- .../imageio/plugins/sgi/SGIImageReader.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReader.java b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReader.java index bbc291db..738666e8 100755 --- a/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReader.java +++ b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReader.java @@ -218,8 +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 - int destY = height - 1 - y; - if (destY % ySub != 0 || destY < srcRegion.y || destY >= 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); } @@ -249,14 +249,14 @@ public final class SGIImageReader extends ImageReaderBase { normalize(rowDataByte, 0, srcRegion.width / xSub); // Flip into position (SGI images are stored bottom/up) - int dstY = (destY - 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 - int destY = height - 1 - y; - if (destY % ySub != 0 || destY < srcRegion.y || destY >= 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); } @@ -286,7 +286,7 @@ public final class SGIImageReader extends ImageReaderBase { normalize(rowDataUShort, 0, srcRegion.width / xSub); // Flip into position (SGI images are stored bottom/up) - int dstY = (destY - srcRegion.y) / ySub; + int dstY = (srcY - srcRegion.y) / ySub; destChannel.setDataElements(0, dstY, srcChannel); } From 85fb9e6af385618071a9329ee3f2b9809e4b1181 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Fri, 26 Feb 2021 17:13:08 +0100 Subject: [PATCH 21/32] JPEG Exif/thumbnail refactoring --- .../imageio/util/ImageReaderAbstractTest.java | 134 ++++++---- .../imageio/plugins/jpeg/Application.java | 6 +- .../{JFIFThumbnailReader.java => EXIF.java} | 50 ++-- .../imageio/plugins/jpeg/EXIFThumbnail.java | 164 ++++++++++++ .../plugins/jpeg/EXIFThumbnailReader.java | 248 ------------------ .../imageio/plugins/jpeg/JFIF.java | 11 +- ...ogressListener.java => JFIFThumbnail.java} | 24 +- .../imageio/plugins/jpeg/JFXX.java | 9 +- .../imageio/plugins/jpeg/JFXXThumbnail.java | 91 +++++++ .../plugins/jpeg/JFXXThumbnailReader.java | 178 ------------- .../jpeg/JPEGImage10MetadataCleaner.java | 3 +- .../imageio/plugins/jpeg/JPEGImageReader.java | 170 +++--------- .../jpeg/JPEGSegmentImageInputStream.java | 6 +- ...r.java => JPEGSegmentWarningListener.java} | 4 +- .../imageio/plugins/jpeg/ThumbnailReader.java | 192 ++++++++++---- .../jpeg/AbstractThumbnailReaderTest.java | 7 +- .../plugins/jpeg/EXIFThumbnailReaderTest.java | 54 +--- .../plugins/jpeg/JFIFThumbnailReaderTest.java | 65 +++-- .../plugins/jpeg/JFXXThumbnailReaderTest.java | 94 +++++-- .../imageio/plugins/tga/TGAImageReader.java | 2 + 20 files changed, 728 insertions(+), 784 deletions(-) rename imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/{JFIFThumbnailReader.java => EXIF.java} (63%) create mode 100644 imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnail.java delete mode 100644 imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java rename imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/{ThumbnailReadProgressListener.java => JFIFThumbnail.java} (68%) create mode 100644 imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnail.java delete mode 100644 imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java rename imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/{JPEGSegmentStreamWarningListener.java => JPEGSegmentWarningListener.java} (92%) diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java index 92bc7025..3043aea6 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java @@ -283,26 +283,6 @@ public abstract class ImageReaderAbstractTest { 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 { 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 @@ -1658,6 +1640,64 @@ public abstract class ImageReaderAbstractTest { 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(); + } + @Test public void testNotBadCachingThumbnails() throws IOException { T reader = createReader(); diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/Application.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/Application.java index 206c7728..b7de325c 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/Application.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/Application.java @@ -38,7 +38,7 @@ import java.io.IOException; import java.io.InputStream; /** - * Application. + * An application (APPn) segment in the JPEG stream. * * @author Harald Kuhr * @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)) { diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIF.java similarity index 63% rename from imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReader.java rename to imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIF.java index 20818057..000046ed 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIF.java @@ -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 Harald Kuhr * @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); } } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnail.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnail.java new file mode 100644 index 00000000..514e6088 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnail.java @@ -0,0 +1,164 @@ +/* + * 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 Harald Kuhr + * @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 exif, final CompoundDirectory exifMetadata, final ImageReader jpegThumbnailReader, final JPEGSegmentWarningListener listener) throws IOException { + if (exif != null && exifMetadata != null && exifMetadata.directoryCount() == 2) { + ImageInputStream stream = exif.exifData(); // NOTE This is an in-memory stream and must not be closed... + + 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(); + + switch (compression) { + case 6: + return createJPEGThumbnailReader(exif, jpegThumbnailReader, listener, stream, ifd1); + case 1: + return createUncompressedThumbnailReader(listener, stream, ifd1); + default: + listener.warningOccurred("EXIF IFD with unknown thumbnail compression (expected 1 or 6): " + compression); + break; + } + } + + return null; + } + + private static UncompressedThumbnailReader createUncompressedThumbnailReader(JPEGSegmentWarningListener listener, 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(); + + // TODO: Decide on warning OR exception! + 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 && ((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); + } + } + + listener.warningOccurred("EXIF IFD with empty or incomplete uncompressed thumbnail"); + return null; + } + + private static JPEGThumbnailReader createJPEGThumbnailReader(EXIF exif, ImageReader jpegThumbnailReader, JPEGSegmentWarningListener listener, 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); + } + } + } + + listener.warningOccurred("EXIF IFD with empty or incomplete JPEG thumbnail"); + return null; + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java deleted file mode 100644 index c435e56b..00000000 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java +++ /dev/null @@ -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 Harald Kuhr - * @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 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); - } - } -} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIF.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIF.java index 7e97f63e..81d086f5 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIF.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIF.java @@ -38,7 +38,7 @@ import java.io.IOException; import java.nio.ByteBuffer; /** - * JFIFSegment + * A JFIF segment. * * @author Harald Kuhr * @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)) ); } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReadProgressListener.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnail.java similarity index 68% rename from imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReadProgressListener.java rename to imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnail.java index 35fe71d0..e5dc432f 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReadProgressListener.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnail.java @@ -30,17 +30,29 @@ package com.twelvemonkeys.imageio.plugins.jpeg; +import com.twelvemonkeys.imageio.plugins.jpeg.ThumbnailReader.UncompressedThumbnailReader; + /** - * ThumbnailReadProgressListener + * JFIFThumbnail * * @author Harald Kuhr * @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, final JPEGSegmentWarningListener listener) { + if (segment != null && segment.xThumbnail > 0 && segment.yThumbnail > 0) { + if (segment.thumbnail == null || segment.thumbnail.length < segment.xThumbnail * segment.yThumbnail) { + listener.warningOccurred("Ignoring truncated JFIF thumbnail"); + } + else { + return new UncompressedThumbnailReader(segment.xThumbnail, segment.yThumbnail, segment.thumbnail); + } + } - void thumbnailComplete(); + return null; + } } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXX.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXX.java index da2a8027..69117f69 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXX.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXX.java @@ -35,7 +35,7 @@ import java.io.IOException; import java.util.Arrays; /** - * JFXXSegment + * A JFXX segment (aka JFIF extension segment). * * @author Harald Kuhr * @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 ); } } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnail.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnail.java new file mode 100644 index 00000000..4cdf9bf7 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnail.java @@ -0,0 +1,91 @@ +/* + * 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.ImageReader; + +/** + * JFXXThumbnailReader + * + * @author Harald Kuhr + * @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, final JPEGSegmentWarningListener listener) { + 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: + listener.warningOccurred(String.format("Unknown JFXX extension code: %d, ignoring thumbnail", segment.extensionCode)); + return null; + } + } + + listener.warningOccurred("JFXX segment truncated, ignoring thumbnail"); + } + + return null; + } +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java deleted file mode 100644 index 6da5f96a..00000000 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java +++ /dev/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 Harald Kuhr - * @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 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); - } -} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java index 6ba45485..c96faf95 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java @@ -32,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(), JPEGSegmentWarningListener.NULL_LISTENER); IIOMetadataNode jfifThumb; switch (jfxx.extensionCode) { diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index af312269..5a4a6b89 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -34,14 +34,10 @@ 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.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.stream.SubImageInputStream; import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; import com.twelvemonkeys.imageio.util.ProgressListenerBase; @@ -62,7 +58,6 @@ import java.awt.color.ICC_ColorSpace; import java.awt.color.ICC_Profile; import java.awt.image.*; import java.io.*; -import java.nio.ByteOrder; import java.util.List; import java.util.*; @@ -667,7 +662,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); } @@ -705,6 +700,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); } @@ -889,25 +885,25 @@ public final class JPEGImageReader extends ImageReaderBase { return jfxx.isEmpty() ? null : (JFXX) jfxx.get(0); } - private CompoundDirectory getExif() throws IOException { - List exifSegments = getAppSegments(JPEG.APP1, "Exif"); + private EXIF getExif() throws IOException { + List 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; @@ -916,7 +912,7 @@ public final class JPEGImageReader extends ImageReaderBase { // TODO: Util method? static byte[] readFully(DataInput stream, int len) throws IOException { if (len == 0) { - return null; + throw new IllegalArgumentException("len == 0"); } byte[] data = new byte[len]; @@ -1089,109 +1085,26 @@ public final class JPEGImageReader extends ImageReaderBase { if (thumbnails == null) { thumbnails = new ArrayList<>(); - ThumbnailReadProgressListener thumbnailProgressDelegator = new ThumbnailProgressDelegate(); + + JPEGSegmentWarningDelegate listenerDelegate = new JPEGSegmentWarningDelegate(); // Read JFIF thumbnails if present - JFIF jfif = getJFIF(); - if (jfif != null && jfif.thumbnail != null) { - // TODO: Check if the JFIF segment really has room for this thumbnail? - thumbnails.add(new JFIFThumbnailReader(thumbnailProgressDelegator, imageIndex, thumbnails.size(), jfif)); + ThumbnailReader thumbnailReader = JFIFThumbnail.from(getJFIF(), listenerDelegate); + if (thumbnailReader != null) { + thumbnails.add(thumbnailReader); } // 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: - // TODO: Check if the JFXX segment really has room for this thumbnail? - thumbnails.add(new JFXXThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), imageIndex, thumbnails.size(), jfxx)); - break; - default: - processWarningOccurred("Unknown JFXX extension code: " + jfxx.extensionCode); - } + thumbnailReader = JFXXThumbnail.from(getJFXX(), getThumbnailReader(), listenerDelegate); + if (thumbnailReader != null) { + thumbnails.add(thumbnailReader); } // Read Exif thumbnails if present - List exifSegments = getAppSegments(JPEG.APP1, "Exif"); - if (!exifSegments.isEmpty()) { - Application exif = exifSegments.get(0); - - // Identifier is "Exif\0" + 1 byte pad - int dataOffset = exif.identifier.length() + 2; - - if (exif.data.length <= dataOffset) { - processWarningOccurred("Exif chunk has no data."); - } - else { - ImageInputStream stream = new ByteArrayImageInputStream(exif.data, dataOffset, exif.data.length - dataOffset); - try { - 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) { - 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 <= stream.length()) { - // Verify first bytes are FFD8 - stream.seek(jpegOffset); - stream.setByteOrder(ByteOrder.BIG_ENDIAN); - if (stream.readUnsignedShort() == JPEG.SOI) { - thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), 0, thumbnails.size(), ifd1, stream)); - } - // TODO: Simplify this warning fallback stuff... - else { - processWarningOccurred("EXIF IFD with empty or incomplete JPEG thumbnail"); - } - } - else { - processWarningOccurred("EXIF IFD with empty or incomplete JPEG thumbnail"); - } - } - else { - processWarningOccurred("EXIF IFD with JPEG thumbnail missing JPEGInterchangeFormat tag"); - } - } - else if (compression == 1) { - Entry stripOffEntry = ifd1.getEntryById(TIFF.TAG_STRIP_OFFSETS); - if (stripOffEntry != null) { - long stripOffset = ((Number) stripOffEntry.getValue()).longValue(); - - if (stripOffset < stream.length()) { - // TODO: Verify length of Exif thumbnail vs length of segment like in JPEG - // ...but this requires so many extra values... Instead move this logic to the - // EXIFThumbnailReader? - thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), 0, thumbnails.size(), ifd1, stream)); - } - else { - processWarningOccurred("EXIF IFD with empty or incomplete uncompressed thumbnail"); - } - } - else { - processWarningOccurred("EXIF IFD with uncompressed thumbnail missing StripOffsets tag"); - } - } - else { - processWarningOccurred("EXIF IFD with unknown compression (expected 1 or 6): " + compression); - } - } - } - catch (IIOException e) { - processWarningOccurred("Exif chunk present, but can't be read: " + e.getMessage()); - } - } + EXIF exif = getExif(); + thumbnailReader = EXIFThumbnail.from(exif, parseExif(exif), getThumbnailReader(), listenerDelegate); + if (thumbnailReader != null) { + thumbnails.add(thumbnailReader); } } } @@ -1234,13 +1147,13 @@ public final class JPEGImageReader extends ImageReaderBase { public BufferedImage readThumbnail(int imageIndex, int thumbnailIndex) throws IOException { checkThumbnailBounds(imageIndex, thumbnailIndex); -// processThumbnailStarted(imageIndex, thumbnailIndex); -// processThumbnailProgress(0f); + processThumbnailStarted(imageIndex, thumbnailIndex); + processThumbnailProgress(0f); BufferedImage thumbnail = thumbnails.get(thumbnailIndex).read();; -// processThumbnailProgress(100f); -// processThumbnailComplete(); + processThumbnailProgress(100f); + processThumbnailComplete(); return thumbnail; } @@ -1251,7 +1164,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 @@ -1376,24 +1289,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); diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java index 37b49462..350f7384 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java @@ -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) { diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentStreamWarningListener.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentWarningListener.java similarity index 92% rename from imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentStreamWarningListener.java rename to imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentWarningListener.java index 7d533280..13c7caf9 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentStreamWarningListener.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentWarningListener.java @@ -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) {} }; diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java index 4fe50160..289fd218 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java @@ -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); } } } diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/AbstractThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/AbstractThumbnailReaderTest.java index c5f0b380..118c4a53 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/AbstractThumbnailReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/AbstractThumbnailReaderTest.java @@ -39,6 +39,7 @@ import java.io.IOException; import java.net.URL; import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; /** * AbstractThumbnailReaderTest @@ -52,9 +53,9 @@ public abstract class AbstractThumbnailReaderTest { IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi()); } - protected abstract ThumbnailReader createReader( - ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, ImageInputStream stream - ) throws IOException; + protected final JPEGSegmentWarningListener listener = mock(JPEGSegmentWarningListener.class); + + protected abstract ThumbnailReader createReader(ImageInputStream stream) throws IOException; protected final ImageInputStream createStream(final String name) throws IOException { URL resource = getClass().getResource(name); diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java index f997935d..1361cd0c 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java @@ -35,18 +35,19 @@ 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 org.junit.Test; -import org.mockito.InOrder; 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.List; import static org.junit.Assert.*; -import static org.mockito.Mockito.*; /** * EXIFThumbnailReaderTest @@ -57,31 +58,28 @@ import static org.mockito.Mockito.*; */ public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest { + private final ImageReader thumbnailReader = ImageIO.getImageReadersByFormatName("jpeg").next(); + @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 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, listener); } @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 +92,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()); @@ -104,28 +102,4 @@ public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest { assertEquals(80, thumbnail.getWidth()); 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(); - } } diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReaderTest.java index 6e780da2..60df7e48 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReaderTest.java @@ -33,8 +33,8 @@ 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 javax.imageio.stream.ImageInputStream; import java.awt.image.BufferedImage; @@ -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 segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP0, "JFIF"); stream.close(); @@ -62,12 +63,54 @@ 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()), listener); + } + + @Test + public void testFromNull() { + assertNull(JFIFThumbnail.from(null, listener)); + + verify(listener, never()).warningOccurred(anyString()); + } + + @Test + public void testFromNullThumbnail() { + assertNull(JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 0, 0, null), listener)); + + verify(listener, never()).warningOccurred(anyString()); + } + + @Test + public void testFromEmpty() { + assertNull(JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 0, 0, new byte[0]), listener)); + + verify(listener, never()).warningOccurred(anyString()); + } + + @Test + public void testFromTruncated() { + assertNull(JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 255, 170, new byte[99]), listener)); + + verify(listener, only()).warningOccurred(anyString()); + } + + @Test + public void testFromValid() throws IOException { + ThumbnailReader reader = JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 30, 20, new byte[30 * 20 * 3]), listener); + assertNotNull(reader); + + verify(listener, never()).warningOccurred(anyString()); + + // 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 +123,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 +134,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(); - } } diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java index d2396cfc..e0c01bc8 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java @@ -33,10 +33,12 @@ 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.After; import org.junit.Test; -import org.mockito.InOrder; import javax.imageio.ImageIO; +import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import java.awt.image.BufferedImage; import java.io.DataInputStream; @@ -44,6 +46,7 @@ import java.io.IOException; import java.util.List; import static org.junit.Assert.*; +import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.*; /** @@ -54,8 +57,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(ImageInputStream stream) throws IOException { List segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP0, "JFXX"); stream.close(); @@ -63,12 +68,81 @@ 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, listener); + } + + @After + public void tearDown() { + thumbnailReader.dispose(); + } + + @Test + public void testFromNull() { + assertNull(JFXXThumbnail.from(null, thumbnailReader, listener)); + + verify(listener, never()).warningOccurred(anyString()); + } + + @Test + public void testFromNullThumbnail() { + assertNull(JFXXThumbnail.from(new JFXX(JFXX.JPEG, null), thumbnailReader, listener)); + + verify(listener, only()).warningOccurred(anyString()); + } + + @Test + public void testFromEmpty() { + assertNull(JFXXThumbnail.from(new JFXX(JFXX.JPEG, new byte[0]), thumbnailReader, listener)); + + verify(listener, only()).warningOccurred(anyString()); + } + + @Test + public void testFromTruncatedJPEG() { + assertNull(JFXXThumbnail.from(new JFXX(JFXX.JPEG, new byte[99]), thumbnailReader, listener)); + + verify(listener, only()).warningOccurred(anyString()); + } + + @Test + public void testFromTruncatedRGB() { + byte[] thumbnail = new byte[765]; + thumbnail[0] = (byte) 160; + thumbnail[1] = 90; + assertNull(JFXXThumbnail.from(new JFXX(JFXX.RGB, thumbnail), thumbnailReader, listener)); + + verify(listener, only()).warningOccurred(anyString()); + } + + @Test + public void testFromTruncatedIndexed() { + byte[] thumbnail = new byte[365]; + thumbnail[0] = (byte) 160; + thumbnail[1] = 90; + assertNull(JFXXThumbnail.from(new JFXX(JFXX.INDEXED, thumbnail), thumbnailReader, listener)); + + verify(listener, only()).warningOccurred(anyString()); + } + + @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, listener); + assertNotNull(reader); + + verify(listener, never()).warningOccurred(anyString()); + + // 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 +155,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(); - } } diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReader.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReader.java index 9c0671b3..17f82958 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReader.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageReader.java @@ -441,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); @@ -468,6 +469,7 @@ final class TGAImageReader extends ImageReaderBase { } } + processThumbnailProgress(100f); processThumbnailComplete(); return destination; From 0286fa42683ba5f2cf5920ebd7bc7d93f7da07ae Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Fri, 26 Feb 2021 18:27:58 +0100 Subject: [PATCH 22/32] JPEG Exif/thumbnail refactoring pt II. --- .../imageio/plugins/jpeg/EXIFThumbnail.java | 38 ++-- .../imageio/plugins/jpeg/JFIFThumbnail.java | 12 +- .../imageio/plugins/jpeg/JFXXThumbnail.java | 13 +- .../jpeg/JPEGImage10MetadataCleaner.java | 2 +- .../imageio/plugins/jpeg/JPEGImageReader.java | 48 ++--- .../jpeg/AbstractThumbnailReaderTest.java | 3 - .../plugins/jpeg/EXIFThumbnailReaderTest.java | 164 +++++++++++++++++- .../plugins/jpeg/JFIFThumbnailReaderTest.java | 34 ++-- .../plugins/jpeg/JFXXThumbnailReaderTest.java | 55 +++--- 9 files changed, 254 insertions(+), 115 deletions(-) diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnail.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnail.java index 514e6088..0cdece31 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnail.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnail.java @@ -57,31 +57,30 @@ final class EXIFThumbnail { private EXIFThumbnail() { } - static ThumbnailReader from(final EXIF exif, final CompoundDirectory exifMetadata, final ImageReader jpegThumbnailReader, final JPEGSegmentWarningListener listener) throws IOException { - if (exif != null && exifMetadata != null && exifMetadata.directoryCount() == 2) { - ImageInputStream stream = exif.exifData(); // NOTE This is an in-memory stream and must not be closed... + 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 = exifMetadata.getDirectory(1); + 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 6: - return createJPEGThumbnailReader(exif, jpegThumbnailReader, listener, stream, ifd1); case 1: - return createUncompressedThumbnailReader(listener, stream, ifd1); + return createUncompressedThumbnailReader(stream, ifd1); + case 6: + return createJPEGThumbnailReader(segment, jpegThumbnailReader, stream, ifd1); default: - listener.warningOccurred("EXIF IFD with unknown thumbnail compression (expected 1 or 6): " + compression); - break; + throw new IIOException("EXIF IFD with unknown thumbnail compression (expected 1 or 6): " + compression); } } return null; } - private static UncompressedThumbnailReader createUncompressedThumbnailReader(JPEGSegmentWarningListener listener, ImageInputStream stream, Directory ifd1) throws IOException { + 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); @@ -95,12 +94,8 @@ final class EXIFThumbnail { int w = ((Number) width.getValue()).intValue(); int h = ((Number) height.getValue()).intValue(); - // TODO: Decide on warning OR exception! - 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 (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) { @@ -111,7 +106,7 @@ final class EXIFThumbnail { long stripOffset = ((Number) stripOffEntry.getValue()).longValue(); int thumbLength = w * h * 3; - if (stripOffset >= 0 && stripOffset + thumbLength < stream.length()) { + if (stripOffset >= 0 && stripOffset + thumbLength <= stream.length()) { // Read raw image data, either RGB or YCbCr stream.seek(stripOffset); byte[] thumbData = new byte[thumbLength]; @@ -135,11 +130,10 @@ final class EXIFThumbnail { } } - listener.warningOccurred("EXIF IFD with empty or incomplete uncompressed thumbnail"); - return null; + throw new IIOException("EXIF IFD with empty or incomplete uncompressed thumbnail"); } - private static JPEGThumbnailReader createJPEGThumbnailReader(EXIF exif, ImageReader jpegThumbnailReader, JPEGSegmentWarningListener listener, ImageInputStream stream, Directory ifd1) throws IOException { + 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); @@ -152,13 +146,13 @@ final class EXIFThumbnail { // Verify first bytes are FFD8 stream.seek(jpegOffset); stream.setByteOrder(ByteOrder.BIG_ENDIAN); + if (stream.readUnsignedShort() == JPEG.SOI) { return new JPEGThumbnailReader(jpegThumbnailReader, stream, jpegOffset); } } } - listener.warningOccurred("EXIF IFD with empty or incomplete JPEG thumbnail"); - return null; + throw new IIOException("EXIF IFD with empty or incomplete JPEG thumbnail"); } } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnail.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnail.java index e5dc432f..a8d5b67b 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnail.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnail.java @@ -32,6 +32,9 @@ package com.twelvemonkeys.imageio.plugins.jpeg; import com.twelvemonkeys.imageio.plugins.jpeg.ThumbnailReader.UncompressedThumbnailReader; +import javax.imageio.IIOException; +import java.io.IOException; + /** * JFIFThumbnail * @@ -43,14 +46,13 @@ final class JFIFThumbnail { private JFIFThumbnail() { } - static ThumbnailReader from(final JFIF segment, final JPEGSegmentWarningListener listener) { + 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) { - listener.warningOccurred("Ignoring truncated JFIF thumbnail"); - } - else { - return new UncompressedThumbnailReader(segment.xThumbnail, segment.yThumbnail, segment.thumbnail); + throw new IIOException("Truncated JFIF thumbnail"); } + + return new UncompressedThumbnailReader(segment.xThumbnail, segment.yThumbnail, segment.thumbnail); } return null; diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnail.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnail.java index 4cdf9bf7..accf9b67 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnail.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnail.java @@ -36,7 +36,9 @@ import com.twelvemonkeys.imageio.plugins.jpeg.ThumbnailReader.JPEGThumbnailReade 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 @@ -50,7 +52,7 @@ final class JFXXThumbnail { private JFXXThumbnail() { } - static ThumbnailReader from(final JFXX segment, final ImageReader thumbnailReader, final JPEGSegmentWarningListener listener) { + 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) { @@ -64,26 +66,29 @@ final class JFXXThumbnail { 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: - listener.warningOccurred(String.format("Unknown JFXX extension code: %d, ignoring thumbnail", segment.extensionCode)); - return null; + throw new IIOException(String.format("Unknown JFXX extension code: %d, ignoring thumbnail", segment.extensionCode)); } } - listener.warningOccurred("JFXX segment truncated, ignoring thumbnail"); + throw new IIOException("JFXX segment truncated, ignoring thumbnail"); } return null; diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java index c96faf95..4843bace 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImage10MetadataCleaner.java @@ -133,7 +133,7 @@ final class JPEGImage10MetadataCleaner { IIOMetadataNode app0JFXX = new IIOMetadataNode("app0JFXX"); app0JFXX.setAttribute("extensionCode", String.valueOf(jfxx.extensionCode)); - ThumbnailReader thumbnailReader = JFXXThumbnail.from(jfxx, reader.getThumbnailReader(), JPEGSegmentWarningListener.NULL_LISTENER); + ThumbnailReader thumbnailReader = JFXXThumbnail.from(jfxx, reader.getThumbnailReader()); IIOMetadataNode jfifThumb; switch (jfxx.extensionCode) { diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 5a4a6b89..c8c7b7d2 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -909,17 +909,6 @@ public final class JPEGImageReader extends ImageReaderBase { return null; } - // TODO: Util method? - static byte[] readFully(DataInput stream, int len) throws IOException { - if (len == 0) { - throw new IllegalArgumentException("len == 0"); - } - - 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) @@ -1086,25 +1075,38 @@ public final class JPEGImageReader extends ImageReaderBase { if (thumbnails == null) { thumbnails = new ArrayList<>(); - JPEGSegmentWarningDelegate listenerDelegate = new JPEGSegmentWarningDelegate(); - // Read JFIF thumbnails if present - ThumbnailReader thumbnailReader = JFIFThumbnail.from(getJFIF(), listenerDelegate); - if (thumbnailReader != null) { - thumbnails.add(thumbnailReader); + try { + ThumbnailReader thumbnail = JFIFThumbnail.from(getJFIF()); + if (thumbnail != null) { + thumbnails.add(thumbnail); + } + } + catch (IOException e) { + processWarningOccurred(e.getMessage()); } // Read JFXX thumbnails if present - thumbnailReader = JFXXThumbnail.from(getJFXX(), getThumbnailReader(), listenerDelegate); - if (thumbnailReader != null) { - thumbnails.add(thumbnailReader); + try { + ThumbnailReader thumbnail = JFXXThumbnail.from(getJFXX(), getThumbnailReader()); + if (thumbnail != null) { + thumbnails.add(thumbnail); + } + } + catch (IOException e) { + processWarningOccurred(e.getMessage()); } // Read Exif thumbnails if present - EXIF exif = getExif(); - thumbnailReader = EXIFThumbnail.from(exif, parseExif(exif), getThumbnailReader(), listenerDelegate); - if (thumbnailReader != null) { - thumbnails.add(thumbnailReader); + try { + EXIF exif = getExif(); + ThumbnailReader thumbnailReader = EXIFThumbnail.from(exif, parseExif(exif), getThumbnailReader()); + if (thumbnailReader != null) { + thumbnails.add(thumbnailReader); + } + } + catch (IOException e) { + processWarningOccurred(e.getMessage()); } } } diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/AbstractThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/AbstractThumbnailReaderTest.java index 118c4a53..70864919 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/AbstractThumbnailReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/AbstractThumbnailReaderTest.java @@ -39,7 +39,6 @@ import java.io.IOException; import java.net.URL; import static org.junit.Assert.assertNotNull; -import static org.mockito.Mockito.mock; /** * AbstractThumbnailReaderTest @@ -53,8 +52,6 @@ public abstract class AbstractThumbnailReaderTest { IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi()); } - protected final JPEGSegmentWarningListener listener = mock(JPEGSegmentWarningListener.class); - protected abstract ThumbnailReader createReader(ImageInputStream stream) throws IOException; protected final ImageInputStream createStream(final String name) throws IOException { diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java index 1361cd0c..5e42c150 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java @@ -30,14 +30,21 @@ 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.After; import org.junit.Test; +import javax.imageio.IIOException; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; @@ -45,6 +52,8 @@ 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.*; @@ -60,6 +69,153 @@ 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.emptyList())), thumbnailReader)); + } + + @Test(expected = IIOException.class) + public void testFromMissingThumbnail() throws IOException { + EXIFThumbnail.from(new EXIF(new byte[42]), new EXIFDirectory(new IFD(Collections.emptyList()), new IFD(Collections.emptyList())), thumbnailReader); + } + + @Test(expected = IIOException.class) + public void testFromUnsupportedThumbnailCompression() throws IOException { + List entries = Collections.singletonList(new TIFFEntry(TIFF.TAG_COMPRESSION, 42)); + EXIFThumbnail.from(new EXIF(new byte[42]), new EXIFDirectory(new IFD(Collections.emptyList()), new IFD(entries)), thumbnailReader); + } + + @Test(expected = IIOException.class) + public void testFromMissingOffsetUncompressed() throws IOException { + List 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.emptyList()), new IFD(entries)), thumbnailReader); + } + + @Test(expected = IIOException.class) + public void testFromMissingWidthUncompressed() throws IOException { + List 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.emptyList()), new IFD(entries)), thumbnailReader); + } + + @Test(expected = IIOException.class) + public void testFromMissingHeightUncompressed() throws IOException { + List 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.emptyList()), new IFD(entries)), thumbnailReader); + } + + @Test(expected = IIOException.class) + public void testFromUnsupportedPhotometricUncompressed() throws IOException { + List 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.emptyList()), new IFD(entries)), thumbnailReader); + } + + @Test(expected = IIOException.class) + public void testFromUnsupportedBitsPerSampleUncompressed() throws IOException { + List 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.emptyList()), new IFD(entries)), thumbnailReader); + } + + @Test(expected = IIOException.class) + public void testFromUnsupportedSamplesPerPixelUncompressed() throws IOException { + List 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.emptyList()), new IFD(entries)), thumbnailReader); + } + + @Test(expected = IIOException.class) + public void testFromTruncatedUncompressed() throws IOException { + List 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.emptyList()), new IFD(entries)), thumbnailReader); + } + + @Test + public void testValidUncompressed() throws IOException { + List 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.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 entries = Collections.singletonList(new TIFFEntry(TIFF.TAG_COMPRESSION, 6)); + EXIFThumbnail.from(new EXIF(new byte[42]), new EXIFDirectory(new IFD(Collections.emptyList()), new IFD(entries)), thumbnailReader); + } + + @Test(expected = IIOException.class) + public void testFromTruncatedJPEG() throws IOException { + List 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.emptyList()), new IFD(entries)), thumbnailReader); + } + + @Override protected ThumbnailReader createReader(final ImageInputStream stream) throws IOException { List segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP1, "Exif"); @@ -74,7 +230,7 @@ public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest { new DataInputStream(data).readFully(exifData); EXIF exif = new EXIF(exifData); - return EXIFThumbnail.from(exif, (CompoundDirectory) new TIFFReader().read(exif.exifData()), thumbnailReader, listener); + return EXIFThumbnail.from(exif, (CompoundDirectory) new TIFFReader().read(exif.exifData()), thumbnailReader); } @Test @@ -102,4 +258,10 @@ public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest { assertEquals(80, thumbnail.getWidth()); assertEquals(60, thumbnail.getHeight()); } + + private static class EXIFDirectory extends AbstractCompoundDirectory { + public EXIFDirectory(IFD... ifds) { + super(Arrays.asList(ifds)); + } + } } diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReaderTest.java index 60df7e48..5a9ade44 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReaderTest.java @@ -36,6 +36,7 @@ import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; 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 @@ -64,44 +64,34 @@ public class JFIFThumbnailReaderTest extends AbstractThumbnailReaderTest { JPEGSegment segment = segments.get(0); - return JFIFThumbnail.from(JFIF.read(new DataInputStream(segment.segmentData()), segment.segmentLength()), listener); + return JFIFThumbnail.from(JFIF.read(new DataInputStream(segment.segmentData()), segment.segmentLength())); } @Test - public void testFromNull() { - assertNull(JFIFThumbnail.from(null, listener)); - - verify(listener, never()).warningOccurred(anyString()); + public void testFromNull() throws IOException { + assertNull(JFIFThumbnail.from(null)); } @Test - public void testFromNullThumbnail() { - assertNull(JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 0, 0, null), listener)); - - verify(listener, never()).warningOccurred(anyString()); + public void testFromNullThumbnail() throws IOException { + assertNull(JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 0, 0, null))); } @Test - public void testFromEmpty() { - assertNull(JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 0, 0, new byte[0]), listener)); - - verify(listener, never()).warningOccurred(anyString()); + public void testFromEmpty() throws IOException { + assertNull(JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 0, 0, new byte[0]))); } - @Test - public void testFromTruncated() { - assertNull(JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 255, 170, new byte[99]), listener)); - - verify(listener, only()).warningOccurred(anyString()); + @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]), listener); + ThumbnailReader reader = JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 30, 20, new byte[30 * 20 * 3])); assertNotNull(reader); - verify(listener, never()).warningOccurred(anyString()); - // Sanity check below assertEquals(30, reader.getWidth()); assertEquals(20, reader.getHeight()); diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java index e0c01bc8..339d3eae 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java @@ -37,6 +37,7 @@ import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; 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; @@ -46,8 +47,6 @@ import java.io.IOException; import java.util.List; import static org.junit.Assert.*; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.*; /** * JFXXThumbnailReaderTest @@ -60,7 +59,7 @@ public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest { private final ImageReader thumbnailReader = ImageIO.getImageReadersByFormatName("jpeg").next(); @Override - protected ThumbnailReader createReader(ImageInputStream stream) throws IOException { + protected ThumbnailReader createReader(final ImageInputStream stream) throws IOException { List segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP0, "JFXX"); stream.close(); @@ -68,7 +67,7 @@ public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest { assertFalse(segments.isEmpty()); JPEGSegment jfxx = segments.get(0); - return JFXXThumbnail.from(JFXX.read(new DataInputStream(jfxx.segmentData()), jfxx.length()), thumbnailReader, listener); + return JFXXThumbnail.from(JFXX.read(new DataInputStream(jfxx.segmentData()), jfxx.length()), thumbnailReader); } @After @@ -77,51 +76,41 @@ public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest { } @Test - public void testFromNull() { - assertNull(JFXXThumbnail.from(null, thumbnailReader, listener)); - - verify(listener, never()).warningOccurred(anyString()); + public void testFromNull() throws IOException { + assertNull(JFXXThumbnail.from(null, thumbnailReader)); } - @Test - public void testFromNullThumbnail() { - assertNull(JFXXThumbnail.from(new JFXX(JFXX.JPEG, null), thumbnailReader, listener)); - - verify(listener, only()).warningOccurred(anyString()); + @Test(expected = IIOException.class) + public void testFromNullThumbnail() throws IOException { + JFXXThumbnail.from(new JFXX(JFXX.JPEG, null), thumbnailReader); } - @Test - public void testFromEmpty() { - assertNull(JFXXThumbnail.from(new JFXX(JFXX.JPEG, new byte[0]), thumbnailReader, listener)); - - verify(listener, only()).warningOccurred(anyString()); + @Test(expected = IIOException.class) + public void testFromEmpty() throws IOException { + JFXXThumbnail.from(new JFXX(JFXX.JPEG, new byte[0]), thumbnailReader); } - @Test - public void testFromTruncatedJPEG() { - assertNull(JFXXThumbnail.from(new JFXX(JFXX.JPEG, new byte[99]), thumbnailReader, listener)); - - verify(listener, only()).warningOccurred(anyString()); + @Test(expected = IIOException.class) + public void testFromTruncatedJPEG() throws IOException { + JFXXThumbnail.from(new JFXX(JFXX.JPEG, new byte[99]), thumbnailReader); } - @Test - public void testFromTruncatedRGB() { + @Test(expected = IIOException.class) + public void testFromTruncatedRGB() throws IOException { byte[] thumbnail = new byte[765]; thumbnail[0] = (byte) 160; thumbnail[1] = 90; - assertNull(JFXXThumbnail.from(new JFXX(JFXX.RGB, thumbnail), thumbnailReader, listener)); - verify(listener, only()).warningOccurred(anyString()); + JFXXThumbnail.from(new JFXX(JFXX.RGB, thumbnail), thumbnailReader); } - @Test - public void testFromTruncatedIndexed() { + @Test(expected = IIOException.class) + public void testFromTruncatedIndexed() throws IOException { byte[] thumbnail = new byte[365]; thumbnail[0] = (byte) 160; thumbnail[1] = 90; - assertNull(JFXXThumbnail.from(new JFXX(JFXX.INDEXED, thumbnail), thumbnailReader, listener)); - verify(listener, only()).warningOccurred(anyString()); + JFXXThumbnail.from(new JFXX(JFXX.INDEXED, thumbnail), thumbnailReader); } @Test @@ -129,11 +118,9 @@ public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest { byte[] thumbnail = new byte[14]; thumbnail[0] = 2; thumbnail[1] = 2; - ThumbnailReader reader = JFXXThumbnail.from(new JFXX(JFXX.RGB, thumbnail), thumbnailReader, listener); + ThumbnailReader reader = JFXXThumbnail.from(new JFXX(JFXX.RGB, thumbnail), thumbnailReader); assertNotNull(reader); - verify(listener, never()).warningOccurred(anyString()); - // Sanity check below assertEquals(2, reader.getWidth()); assertEquals(2, reader.getHeight()); From 20a785ea5e6e3b02a387b48abbf039a1849ebff7 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Fri, 26 Feb 2021 19:36:49 +0100 Subject: [PATCH 23/32] Updated version numbers. --- README.md | 64 +++++++++++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 8539dfe3..8a26e594 100644 --- a/README.md +++ b/README.md @@ -268,12 +268,12 @@ To depend on the JPEG and TIFF plugin using Maven, add the following to your POM com.twelvemonkeys.imageio imageio-jpeg - 3.6.2 + 3.6.3 com.twelvemonkeys.imageio imageio-tiff - 3.6.2 + 3.6.3 *g=qyZFo8wqIv^{ot7>N~ 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 aP^mI)QS$$NS@0_DzkmMQ0{?$4@P7cVMq6V5 literal 0 HcmV?d00001 diff --git a/imageio/imageio-pict/src/test/resources/mac/porsches.mac b/imageio/imageio-pict/src/test/resources/mac/porsches.mac new file mode 100644 index 0000000000000000000000000000000000000000..e50096acefc4b55a7b7250e572230a2e1bb02434 GIT binary patch literal 16512 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 literal 0 HcmV?d00001