From 85fb9e6af385618071a9329ee3f2b9809e4b1181 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Fri, 26 Feb 2021 17:13:08 +0100 Subject: [PATCH] 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;