JPEG Exif/thumbnail refactoring

This commit is contained in:
Harald Kuhr 2021-02-26 17:13:08 +01:00
parent 97a8806bfb
commit 85fb9e6af3
20 changed files with 728 additions and 784 deletions

View File

@ -283,26 +283,6 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
reader.dispose();
}
@Test
public void testReadNoInput() throws IOException {
ImageReader reader = createReader();
// Do not set input
BufferedImage image = null;
try {
image = reader.read(0);
fail("Read image with no input");
}
catch (IllegalStateException ignore) {
}
catch (IOException e) {
failBecause("Image could not be read", e);
}
assertNull(image);
reader.dispose();
}
@Test
public void testReRead() throws IOException {
ImageReader reader = createReader();
@ -323,69 +303,71 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
reader.dispose();
}
@Test
@Test(expected = IllegalStateException.class)
public void testReadNoInput() throws IOException {
ImageReader reader = createReader();
// Do not set input
try {
reader.read(0);
fail("Read image with no input");
}
catch (IOException e) {
failBecause("Image could not be read", e);
}
}
@Test(expected = IndexOutOfBoundsException.class)
public void testReadIndexNegativeWithParam() throws IOException {
ImageReader reader = createReader();
TestData data = getTestData().get(0);
reader.setInput(data.getInputStream());
BufferedImage image = null;
try {
image = reader.read(-1, reader.getDefaultReadParam());
reader.read(-1, reader.getDefaultReadParam());
fail("Read image with illegal index");
}
catch (IndexOutOfBoundsException ignore) {
}
catch (IOException e) {
failBecause("Image could not be read", e);
}
assertNull(image);
reader.dispose();
finally {
reader.dispose();
}
}
@Test
@Test(expected = IndexOutOfBoundsException.class)
public void testReadIndexOutOfBoundsWithParam() throws IOException {
ImageReader reader = createReader();
TestData data = getTestData().get(0);
reader.setInput(data.getInputStream());
BufferedImage image = null;
try {
image = reader.read(Short.MAX_VALUE, reader.getDefaultReadParam());
reader.read(Short.MAX_VALUE, reader.getDefaultReadParam());
fail("Read image with index out of bounds");
}
catch (IndexOutOfBoundsException ignore) {
}
catch (IOException e) {
failBecause("Image could not be read", e);
}
assertNull(image);
reader.dispose();
finally {
reader.dispose();
}
}
@Test
@Test(expected = IllegalStateException.class)
public void testReadNoInputWithParam() throws IOException {
ImageReader reader = createReader();
// Do not set input
BufferedImage image = null;
try {
image = reader.read(0, reader.getDefaultReadParam());
reader.read(0, reader.getDefaultReadParam());
fail("Read image with no input");
}
catch (IllegalStateException ignore) {
}
catch (IOException e) {
failBecause("Image could not be read", e);
}
assertNull(image);
reader.dispose();
finally {
reader.dispose();
}
}
@Test
@ -1658,6 +1640,64 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
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();

View File

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

View File

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

View File

@ -0,0 +1,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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: EXIFThumbnail.java,v 1.0 18.04.12 12:19 haraldk Exp$
*/
final class EXIFThumbnail {
private EXIFThumbnail() {
}
static ThumbnailReader from(final EXIF 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;
}
}

View File

@ -1,248 +0,0 @@
/*
* Copyright (c) 2012, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.color.YCbCrConverter;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.lang.Validate;
import javax.imageio.IIOException;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.lang.ref.SoftReference;
import java.util.Arrays;
/**
* EXIFThumbnail
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: EXIFThumbnail.java,v 1.0 18.04.12 12:19 haraldk Exp$
*/
final class EXIFThumbnailReader extends ThumbnailReader {
private final ImageReader reader;
private final Directory ifd;
private final ImageInputStream stream;
private final int compression;
private transient SoftReference<BufferedImage> cachedThumbnail;
EXIFThumbnailReader(final ThumbnailReadProgressListener progressListener, final ImageReader jpegReader, final int imageIndex, final int thumbnailIndex, final Directory ifd, final ImageInputStream stream) {
super(progressListener, imageIndex, thumbnailIndex);
this.reader = Validate.notNull(jpegReader);
this.ifd = ifd;
this.stream = stream;
Entry compression = ifd.getEntryById(TIFF.TAG_COMPRESSION);
this.compression = compression != null ? ((Number) compression.getValue()).intValue() : 6;
}
@Override
public BufferedImage read() throws IOException {
if (compression == 1) { // 1 = no compression
processThumbnailStarted();
BufferedImage thumbnail = readUncompressed();
processThumbnailProgress(100f);
processThumbnailComplete();
return thumbnail;
}
else if (compression == 6) { // 6 = JPEG compression
processThumbnailStarted();
BufferedImage thumbnail = readJPEGCached(true);
processThumbnailProgress(100f);
processThumbnailComplete();
return thumbnail;
}
else {
throw new IIOException("Unsupported EXIF thumbnail compression: " + compression);
}
}
private BufferedImage readJPEGCached(final boolean pixelsExposed) throws IOException {
BufferedImage thumbnail = cachedThumbnail != null ? cachedThumbnail.get() : null;
if (thumbnail == null) {
thumbnail = readJPEG();
}
cachedThumbnail = pixelsExposed ? null : new SoftReference<>(thumbnail);
return thumbnail;
}
private BufferedImage readJPEG() throws IOException {
// IFD1 should contain JPEG offset for JPEG thumbnail
Entry jpegOffset = ifd.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT);
if (jpegOffset != null) {
stream.seek(((Number) jpegOffset.getValue()).longValue());
InputStream input = IIOUtil.createStreamAdapter(stream);
// For certain EXIF files (encoded with TIFF.TAG_YCBCR_POSITIONING = 2?), we need
// EXIF information to read the thumbnail correctly (otherwise the colors are messed up).
// Probably related to: http://bugs.sun.com/view_bug.do?bug_id=4881314
// HACK: Splice empty EXIF information into the thumbnail stream
byte[] fakeEmptyExif = {
// SOI (from original data)
(byte) input.read(), (byte) input.read(),
// APP1 + len (016) + 'Exif' + 0-term + pad
(byte) 0xFF, (byte) 0xE1, 0, 16, 'E', 'x', 'i', 'f', 0, 0,
// Big-endian BOM (MM), TIFF magic (042), offset (0000)
'M', 'M', 0, 42, 0, 0, 0, 0,
};
input = new SequenceInputStream(new ByteArrayInputStream(fakeEmptyExif), input);
try {
try (MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(input)) {
return readJPEGThumbnail(reader, stream);
}
}
finally {
input.close();
}
}
throw new IIOException("Missing JPEGInterchangeFormat tag for JPEG compressed EXIF thumbnail");
}
private BufferedImage readUncompressed() throws IOException {
// Read ImageWidth, ImageLength (height) and BitsPerSample (=8 8 8, always)
// PhotometricInterpretation (2=RGB, 6=YCbCr), SamplesPerPixel (=3, always),
Entry width = ifd.getEntryById(TIFF.TAG_IMAGE_WIDTH);
Entry height = ifd.getEntryById(TIFF.TAG_IMAGE_HEIGHT);
if (width == null || height == null) {
throw new IIOException("Missing dimensions for uncompressed EXIF thumbnail");
}
Entry bitsPerSample = ifd.getEntryById(TIFF.TAG_BITS_PER_SAMPLE);
Entry samplesPerPixel = ifd.getEntryById(TIFF.TAG_SAMPLES_PER_PIXEL);
Entry photometricInterpretation = ifd.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION);
// Required
int w = ((Number) width.getValue()).intValue();
int h = ((Number) height.getValue()).intValue();
if (bitsPerSample != null) {
int[] bpp = (int[]) bitsPerSample.getValue();
if (!Arrays.equals(bpp, new int[] {8, 8, 8})) {
throw new IIOException("Unknown BitsPerSample value for uncompressed EXIF thumbnail (expected [8, 8, 8]): " + bitsPerSample.getValueAsString());
}
}
if (samplesPerPixel != null && (Integer) samplesPerPixel.getValue() != 3) {
throw new IIOException("Unknown SamplesPerPixel value for uncompressed EXIF thumbnail (expected 3): " + samplesPerPixel.getValueAsString());
}
int interpretation = photometricInterpretation != null ? ((Number) photometricInterpretation.getValue()).intValue() : 2;
// IFD1 should contain strip offsets for uncompressed images
Entry offset = ifd.getEntryById(TIFF.TAG_STRIP_OFFSETS);
if (offset != null) {
stream.seek(((Number) offset.getValue()).longValue());
// Read raw image data, either RGB or YCbCr
int thumbSize = w * h * 3;
byte[] thumbData = JPEGImageReader.readFully(stream, thumbSize);
switch (interpretation) {
case 2:
// RGB
break;
case 6:
// YCbCr
for (int i = 0; i < thumbSize; i += 3) {
YCbCrConverter.convertYCbCr2RGB(thumbData, thumbData, i);
}
break;
default:
throw new IIOException("Unknown PhotometricInterpretation value for uncompressed EXIF thumbnail (expected 2 or 6): " + interpretation);
}
return ThumbnailReader.readRawThumbnail(thumbData, thumbSize, 0, w, h);
}
throw new IIOException("Missing StripOffsets tag for uncompressed EXIF thumbnail");
}
@Override
public int getWidth() throws IOException {
if (compression == 1) { // 1 = no compression
Entry width = ifd.getEntryById(TIFF.TAG_IMAGE_WIDTH);
if (width == null) {
throw new IIOException("Missing dimensions for uncompressed EXIF thumbnail");
}
return ((Number) width.getValue()).intValue();
}
else if (compression == 6) { // 6 = JPEG compression
return readJPEGCached(false).getWidth();
}
else {
throw new IIOException("Unsupported EXIF thumbnail compression (expected 1 or 6): " + compression);
}
}
@Override
public int getHeight() throws IOException {
if (compression == 1) { // 1 = no compression
Entry height = ifd.getEntryById(TIFF.TAG_IMAGE_HEIGHT);
if (height == null) {
throw new IIOException("Missing dimensions for uncompressed EXIF thumbnail");
}
return ((Number) height.getValue()).intValue();
}
else if (compression == 6) { // 6 = JPEG compression
return readJPEGCached(false).getHeight();
}
else {
throw new IIOException("Unsupported EXIF thumbnail compression (expected 1 or 6): " + compression);
}
}
}

View File

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

View File

@ -30,17 +30,29 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.plugins.jpeg.ThumbnailReader.UncompressedThumbnailReader;
/**
* ThumbnailReadProgressListener
* JFIFThumbnail
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: ThumbnailReadProgressListener.java,v 1.0 07.05.12 10:15 haraldk Exp$
* @version $Id: JFIFThumbnail.java,v 1.0 18.04.12 12:19 haraldk Exp$
*/
interface ThumbnailReadProgressListener {
void thumbnailStarted(int imageIndex, int thumbnailIndex);
final class JFIFThumbnail {
private JFIFThumbnail() {
}
void thumbnailProgress(float percentageDone);
static ThumbnailReader from(final JFIF segment, 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;
}
}

View File

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

View File

@ -0,0 +1,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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: JFXXThumbnailReader.java,v 1.0 18.04.12 12:19 haraldk Exp$
*/
final class JFXXThumbnail {
private JFXXThumbnail() {
}
static ThumbnailReader from(final JFXX segment, final ImageReader thumbnailReader, 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;
}
}

View File

@ -1,178 +0,0 @@
/*
* Copyright (c) 2012, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.image.InverseColorMapIndexColorModel;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.lang.Validate;
import javax.imageio.IIOException;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.*;
import java.io.IOException;
import java.lang.ref.SoftReference;
/**
* JFXXThumbnailReader
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: JFXXThumbnailReader.java,v 1.0 18.04.12 12:19 haraldk Exp$
*/
final class JFXXThumbnailReader extends ThumbnailReader {
private final ImageReader reader;
private final JFXX segment;
private transient SoftReference<BufferedImage> cachedThumbnail;
JFXXThumbnailReader(final ThumbnailReadProgressListener progressListener, final ImageReader jpegReader, final int imageIndex, final int thumbnailIndex, final JFXX segment) {
super(progressListener, imageIndex, thumbnailIndex);
this.reader = Validate.notNull(jpegReader);
this.segment = segment;
}
@Override
public BufferedImage read() throws IOException {
processThumbnailStarted();
BufferedImage thumbnail;
switch (segment.extensionCode) {
case JFXX.JPEG:
thumbnail = readJPEGCached(true);
break;
case JFXX.INDEXED:
thumbnail = readIndexed();
break;
case JFXX.RGB:
thumbnail = readRGB();
break;
default:
throw new IIOException(String.format("Unsupported JFXX extension code: %d", segment.extensionCode));
}
processThumbnailProgress(100f);
processThumbnailComplete();
return thumbnail;
}
IIOMetadata readMetadata() throws IOException {
ImageInputStream input = new ByteArrayImageInputStream(segment.thumbnail);
try {
reader.setInput(input);
return reader.getImageMetadata(0);
}
finally {
input.close();
}
}
private BufferedImage readJPEGCached(boolean pixelsExposed) throws IOException {
BufferedImage thumbnail = cachedThumbnail != null ? cachedThumbnail.get() : null;
if (thumbnail == null) {
ImageInputStream stream = new ByteArrayImageInputStream(segment.thumbnail);
try {
thumbnail = readJPEGThumbnail(reader, stream);
}
finally {
stream.close();
}
}
cachedThumbnail = pixelsExposed ? null : new SoftReference<>(thumbnail);
return thumbnail;
}
@Override
public int getWidth() throws IOException {
switch (segment.extensionCode) {
case JFXX.RGB:
case JFXX.INDEXED:
return segment.thumbnail[0] & 0xff;
case JFXX.JPEG:
return readJPEGCached(false).getWidth();
default:
throw new IIOException(String.format("Unsupported JFXX extension code: %d", segment.extensionCode));
}
}
@Override
public int getHeight() throws IOException {
switch (segment.extensionCode) {
case JFXX.RGB:
case JFXX.INDEXED:
return segment.thumbnail[1] & 0xff;
case JFXX.JPEG:
return readJPEGCached(false).getHeight();
default:
throw new IIOException(String.format("Unsupported JFXX extension code: %d", segment.extensionCode));
}
}
private BufferedImage readIndexed() {
// 1 byte: xThumb
// 1 byte: yThumb
// 768 bytes: palette
// x * y bytes: 8 bit indexed pixels
int w = segment.thumbnail[0] & 0xff;
int h = segment.thumbnail[1] & 0xff;
int[] rgbs = new int[256];
for (int i = 0; i < rgbs.length; i++) {
rgbs[i] = (segment.thumbnail[3 * i + 2] & 0xff) << 16
| (segment.thumbnail[3 * i + 3] & 0xff) << 8
| (segment.thumbnail[3 * i + 4] & 0xff);
}
IndexColorModel icm = new InverseColorMapIndexColorModel(8, rgbs.length, rgbs, 0, false, -1, DataBuffer.TYPE_BYTE);
DataBufferByte buffer = new DataBufferByte(segment.thumbnail, segment.thumbnail.length - 770, 770);
WritableRaster raster = Raster.createPackedRaster(buffer, w, h, 8, null);
return new BufferedImage(icm, raster, icm.isAlphaPremultiplied(), null);
}
private BufferedImage readRGB() {
// 1 byte: xThumb
// 1 byte: yThumb
// 3 * x * y bytes: 24 bit RGB pixels
int w = segment.thumbnail[0] & 0xff;
int h = segment.thumbnail[1] & 0xff;
return ThumbnailReader.readRawThumbnail(segment.thumbnail, segment.thumbnail.length - 2, 2, w, h);
}
}

View File

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

View File

@ -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<Application> exifSegments = getAppSegments(JPEG.APP1, "Exif");
private EXIF getExif() throws IOException {
List<Application> exif = getAppSegments(JPEG.APP1, "Exif");
return exif.isEmpty() ? null : (EXIF) exif.get(0); // TODO: Can there actually be more Exif segments?
}
if (!exifSegments.isEmpty()) {
Application exif = exifSegments.get(0);
int offset = exif.identifier.length() + 2; // Incl. pad
if (exif.data.length <= offset) {
processWarningOccurred("Exif chunk has no data.");
}
else {
// TODO: Consider returning ByteArrayImageInputStream from Segment.data()
try (ImageInputStream stream = new ByteArrayImageInputStream(exif.data, offset, exif.data.length - offset)) {
private CompoundDirectory parseExif(final EXIF exif) throws IOException {
if (exif != null) {
// Identifier is "Exif\0" + 1 byte pad
if (exif.data.length > exif.identifier.length() + 2) {
try (ImageInputStream stream = exif.exifData()) {
return (CompoundDirectory) new TIFFReader().read(stream);
}
catch (IIOException e) {
processWarningOccurred("Exif chunk is present, but can't be read: " + e.getMessage());
}
}
else {
processWarningOccurred("Exif chunk has no data.");
}
}
return null;
@ -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<Application> 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);

View File

@ -59,7 +59,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
// TODO: Support multiple JPEG streams (SOI...EOI, SOI...EOI, ...) in a single file
private final ImageInputStream stream;
private final JPEGSegmentStreamWarningListener warningListener;
private final JPEGSegmentWarningListener warningListener;
private final ComponentIdSet componentIds = new ComponentIdSet();
@ -68,13 +68,13 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
private Segment segment;
JPEGSegmentImageInputStream(final ImageInputStream stream, final JPEGSegmentStreamWarningListener warningListener) {
JPEGSegmentImageInputStream(final ImageInputStream stream, final JPEGSegmentWarningListener warningListener) {
this.stream = notNull(stream, "stream");
this.warningListener = notNull(warningListener, "warningListener");
}
JPEGSegmentImageInputStream(final ImageInputStream stream) {
this(stream, JPEGSegmentStreamWarningListener.NULL_LISTENER);
this(stream, JPEGSegmentWarningListener.NULL_LISTENER);
}
private void processWarningOccured(final String warning) {

View File

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

View File

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

View File

@ -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);

View File

@ -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<JPEGSegment> segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP1, "Exif");
stream.close();
assertNotNull(segments);
assertFalse(segments.isEmpty());
TIFFReader reader = new TIFFReader();
InputStream data = segments.get(0).data();
if (data.read() < 0) {
throw new AssertionError("EOF!");
}
JPEGSegment exifSegment = segments.get(0);
InputStream data = exifSegment.segmentData();
byte[] exifData = new byte[exifSegment.segmentLength() - 2];
new DataInputStream(data).readFully(exifData);
ImageInputStream exifStream = ImageIO.createImageInputStream(data);
CompoundDirectory ifds = (CompoundDirectory) reader.read(exifStream);
assertEquals(2, ifds.directoryCount());
return new EXIFThumbnailReader(progressListener, ImageIO.getImageReadersByFormatName("JPEG").next(), imageIndex, thumbnailIndex, ifds.getDirectory(1), exifStream);
EXIF exif = new EXIF(exifData);
return EXIFThumbnail.from(exif, (CompoundDirectory) new TIFFReader().read(exif.exifData()), thumbnailReader, 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();
}
}

View File

@ -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<JPEGSegment> 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();
}
}

View File

@ -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<JPEGSegment> 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();
}
}

View File

@ -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;