mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-06 04:55:30 -04:00
JPEG Exif/thumbnail refactoring
This commit is contained in:
parent
97a8806bfb
commit
85fb9e6af3
@ -283,26 +283,6 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
|
|||||||
reader.dispose();
|
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
|
@Test
|
||||||
public void testReRead() throws IOException {
|
public void testReRead() throws IOException {
|
||||||
ImageReader reader = createReader();
|
ImageReader reader = createReader();
|
||||||
@ -323,69 +303,71 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
|
|||||||
reader.dispose();
|
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 {
|
public void testReadIndexNegativeWithParam() throws IOException {
|
||||||
ImageReader reader = createReader();
|
ImageReader reader = createReader();
|
||||||
TestData data = getTestData().get(0);
|
TestData data = getTestData().get(0);
|
||||||
reader.setInput(data.getInputStream());
|
reader.setInput(data.getInputStream());
|
||||||
|
|
||||||
BufferedImage image = null;
|
|
||||||
try {
|
try {
|
||||||
image = reader.read(-1, reader.getDefaultReadParam());
|
reader.read(-1, reader.getDefaultReadParam());
|
||||||
fail("Read image with illegal index");
|
fail("Read image with illegal index");
|
||||||
}
|
}
|
||||||
catch (IndexOutOfBoundsException ignore) {
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
failBecause("Image could not be read", e);
|
failBecause("Image could not be read", e);
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
assertNull(image);
|
reader.dispose();
|
||||||
|
}
|
||||||
reader.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test(expected = IndexOutOfBoundsException.class)
|
||||||
public void testReadIndexOutOfBoundsWithParam() throws IOException {
|
public void testReadIndexOutOfBoundsWithParam() throws IOException {
|
||||||
ImageReader reader = createReader();
|
ImageReader reader = createReader();
|
||||||
TestData data = getTestData().get(0);
|
TestData data = getTestData().get(0);
|
||||||
reader.setInput(data.getInputStream());
|
reader.setInput(data.getInputStream());
|
||||||
|
|
||||||
BufferedImage image = null;
|
|
||||||
try {
|
try {
|
||||||
image = reader.read(Short.MAX_VALUE, reader.getDefaultReadParam());
|
reader.read(Short.MAX_VALUE, reader.getDefaultReadParam());
|
||||||
fail("Read image with index out of bounds");
|
fail("Read image with index out of bounds");
|
||||||
}
|
}
|
||||||
catch (IndexOutOfBoundsException ignore) {
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
failBecause("Image could not be read", e);
|
failBecause("Image could not be read", e);
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
assertNull(image);
|
reader.dispose();
|
||||||
|
}
|
||||||
reader.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test(expected = IllegalStateException.class)
|
||||||
public void testReadNoInputWithParam() throws IOException {
|
public void testReadNoInputWithParam() throws IOException {
|
||||||
ImageReader reader = createReader();
|
ImageReader reader = createReader();
|
||||||
// Do not set input
|
// Do not set input
|
||||||
|
|
||||||
BufferedImage image = null;
|
|
||||||
try {
|
try {
|
||||||
image = reader.read(0, reader.getDefaultReadParam());
|
reader.read(0, reader.getDefaultReadParam());
|
||||||
fail("Read image with no input");
|
fail("Read image with no input");
|
||||||
}
|
}
|
||||||
catch (IllegalStateException ignore) {
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
failBecause("Image could not be read", e);
|
failBecause("Image could not be read", e);
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
assertNull(image);
|
reader.dispose();
|
||||||
|
}
|
||||||
reader.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -1658,6 +1640,64 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
|
|||||||
reader.dispose();
|
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
|
@Test
|
||||||
public void testNotBadCachingThumbnails() throws IOException {
|
public void testNotBadCachingThumbnails() throws IOException {
|
||||||
T reader = createReader();
|
T reader = createReader();
|
||||||
|
@ -38,7 +38,7 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
* @author last modified by $Author: harald.kuhr$
|
* @author last modified by $Author: harald.kuhr$
|
||||||
@ -78,7 +78,9 @@ class Application extends Segment {
|
|||||||
if ("JFXX".equals(identifier)) {
|
if ("JFXX".equals(identifier)) {
|
||||||
return JFXX.read(data, length);
|
return JFXX.read(data, length);
|
||||||
}
|
}
|
||||||
// TODO: Exif?
|
if ("Exif".equals(identifier)) {
|
||||||
|
return EXIF.read(data, length);
|
||||||
|
}
|
||||||
case JPEG.APP2:
|
case JPEG.APP2:
|
||||||
// ICC_PROFILE
|
// ICC_PROFILE
|
||||||
if ("ICC_PROFILE".equals(identifier)) {
|
if ("ICC_PROFILE".equals(identifier)) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2012, Harald Kuhr
|
* Copyright (c) 2021, Harald Kuhr
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
*
|
||||||
* Redistribution and use in source and binary forms, with or without
|
* Redistribution and use in source and binary forms, with or without
|
||||||
@ -30,41 +30,45 @@
|
|||||||
|
|
||||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
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;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JFIFThumbnailReader
|
* An EXIF segment.
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
* @author last modified by $Author: haraldk$
|
* @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 {
|
final class EXIF extends Application {
|
||||||
private final JFIF segment;
|
EXIF(byte[] data) {
|
||||||
|
super(JPEG.APP1, "Exif", data);
|
||||||
JFIFThumbnailReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex, final JFIF segment) {
|
|
||||||
super(progressListener, imageIndex, thumbnailIndex);
|
|
||||||
this.segment = segment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BufferedImage read() {
|
public String toString() {
|
||||||
processThumbnailStarted();
|
return String.format("APP1/Exif, length: %d", data.length);
|
||||||
BufferedImage thumbnail = readRawThumbnail(segment.thumbnail, segment.thumbnail.length, 0, segment.xThumbnail, segment.yThumbnail);
|
|
||||||
processThumbnailProgress(100f);
|
|
||||||
processThumbnailComplete();
|
|
||||||
|
|
||||||
return thumbnail;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
ImageInputStream exifData() {
|
||||||
public int getWidth() throws IOException {
|
// Identifier is "Exif\0" + 1 byte pad
|
||||||
return segment.xThumbnail;
|
int offset = identifier.length() + 2;
|
||||||
|
return new ByteArrayImageInputStream(data, offset, data.length - offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public static EXIF read(final DataInput data, int length) throws IOException {
|
||||||
public int getHeight() throws IOException {
|
if (length < 2 + 6) {
|
||||||
return segment.yThumbnail;
|
throw new EOFException();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = new byte[length - 2];
|
||||||
|
data.readFully(bytes);
|
||||||
|
|
||||||
|
return new EXIF(bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,248 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2012, Harald Kuhr
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
*
|
|
||||||
* * Redistributions of source code must retain the above copyright notice, this
|
|
||||||
* list of conditions and the following disclaimer.
|
|
||||||
*
|
|
||||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
|
||||||
* and/or other materials provided with the distribution.
|
|
||||||
*
|
|
||||||
* * Neither the name of the copyright holder nor the names of its
|
|
||||||
* contributors may be used to endorse or promote products derived from
|
|
||||||
* this software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
|
||||||
|
|
||||||
import com.twelvemonkeys.imageio.color.YCbCrConverter;
|
|
||||||
import com.twelvemonkeys.imageio.metadata.Directory;
|
|
||||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
|
||||||
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
|
|
||||||
import com.twelvemonkeys.imageio.util.IIOUtil;
|
|
||||||
import com.twelvemonkeys.lang.Validate;
|
|
||||||
|
|
||||||
import javax.imageio.IIOException;
|
|
||||||
import javax.imageio.ImageReader;
|
|
||||||
import javax.imageio.stream.ImageInputStream;
|
|
||||||
import javax.imageio.stream.MemoryCacheImageInputStream;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.SequenceInputStream;
|
|
||||||
import java.lang.ref.SoftReference;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EXIFThumbnail
|
|
||||||
*
|
|
||||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
|
||||||
* @author last modified by $Author: haraldk$
|
|
||||||
* @version $Id: EXIFThumbnail.java,v 1.0 18.04.12 12:19 haraldk Exp$
|
|
||||||
*/
|
|
||||||
final class EXIFThumbnailReader extends ThumbnailReader {
|
|
||||||
private final ImageReader reader;
|
|
||||||
private final Directory ifd;
|
|
||||||
private final ImageInputStream stream;
|
|
||||||
private final int compression;
|
|
||||||
|
|
||||||
private transient SoftReference<BufferedImage> cachedThumbnail;
|
|
||||||
|
|
||||||
EXIFThumbnailReader(final ThumbnailReadProgressListener progressListener, final ImageReader jpegReader, final int imageIndex, final int thumbnailIndex, final Directory ifd, final ImageInputStream stream) {
|
|
||||||
super(progressListener, imageIndex, thumbnailIndex);
|
|
||||||
this.reader = Validate.notNull(jpegReader);
|
|
||||||
this.ifd = ifd;
|
|
||||||
this.stream = stream;
|
|
||||||
|
|
||||||
Entry compression = ifd.getEntryById(TIFF.TAG_COMPRESSION);
|
|
||||||
|
|
||||||
this.compression = compression != null ? ((Number) compression.getValue()).intValue() : 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BufferedImage read() throws IOException {
|
|
||||||
if (compression == 1) { // 1 = no compression
|
|
||||||
processThumbnailStarted();
|
|
||||||
BufferedImage thumbnail = readUncompressed();
|
|
||||||
processThumbnailProgress(100f);
|
|
||||||
processThumbnailComplete();
|
|
||||||
|
|
||||||
return thumbnail;
|
|
||||||
}
|
|
||||||
else if (compression == 6) { // 6 = JPEG compression
|
|
||||||
processThumbnailStarted();
|
|
||||||
BufferedImage thumbnail = readJPEGCached(true);
|
|
||||||
processThumbnailProgress(100f);
|
|
||||||
processThumbnailComplete();
|
|
||||||
|
|
||||||
return thumbnail;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new IIOException("Unsupported EXIF thumbnail compression: " + compression);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private BufferedImage readJPEGCached(final boolean pixelsExposed) throws IOException {
|
|
||||||
BufferedImage thumbnail = cachedThumbnail != null ? cachedThumbnail.get() : null;
|
|
||||||
|
|
||||||
if (thumbnail == null) {
|
|
||||||
thumbnail = readJPEG();
|
|
||||||
}
|
|
||||||
|
|
||||||
cachedThumbnail = pixelsExposed ? null : new SoftReference<>(thumbnail);
|
|
||||||
|
|
||||||
return thumbnail;
|
|
||||||
}
|
|
||||||
|
|
||||||
private BufferedImage readJPEG() throws IOException {
|
|
||||||
// IFD1 should contain JPEG offset for JPEG thumbnail
|
|
||||||
Entry jpegOffset = ifd.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT);
|
|
||||||
|
|
||||||
if (jpegOffset != null) {
|
|
||||||
stream.seek(((Number) jpegOffset.getValue()).longValue());
|
|
||||||
InputStream input = IIOUtil.createStreamAdapter(stream);
|
|
||||||
|
|
||||||
// For certain EXIF files (encoded with TIFF.TAG_YCBCR_POSITIONING = 2?), we need
|
|
||||||
// EXIF information to read the thumbnail correctly (otherwise the colors are messed up).
|
|
||||||
// Probably related to: http://bugs.sun.com/view_bug.do?bug_id=4881314
|
|
||||||
|
|
||||||
// HACK: Splice empty EXIF information into the thumbnail stream
|
|
||||||
byte[] fakeEmptyExif = {
|
|
||||||
// SOI (from original data)
|
|
||||||
(byte) input.read(), (byte) input.read(),
|
|
||||||
// APP1 + len (016) + 'Exif' + 0-term + pad
|
|
||||||
(byte) 0xFF, (byte) 0xE1, 0, 16, 'E', 'x', 'i', 'f', 0, 0,
|
|
||||||
// Big-endian BOM (MM), TIFF magic (042), offset (0000)
|
|
||||||
'M', 'M', 0, 42, 0, 0, 0, 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
input = new SequenceInputStream(new ByteArrayInputStream(fakeEmptyExif), input);
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
try (MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(input)) {
|
|
||||||
return readJPEGThumbnail(reader, stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
input.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new IIOException("Missing JPEGInterchangeFormat tag for JPEG compressed EXIF thumbnail");
|
|
||||||
}
|
|
||||||
|
|
||||||
private BufferedImage readUncompressed() throws IOException {
|
|
||||||
// Read ImageWidth, ImageLength (height) and BitsPerSample (=8 8 8, always)
|
|
||||||
// PhotometricInterpretation (2=RGB, 6=YCbCr), SamplesPerPixel (=3, always),
|
|
||||||
Entry width = ifd.getEntryById(TIFF.TAG_IMAGE_WIDTH);
|
|
||||||
Entry height = ifd.getEntryById(TIFF.TAG_IMAGE_HEIGHT);
|
|
||||||
|
|
||||||
if (width == null || height == null) {
|
|
||||||
throw new IIOException("Missing dimensions for uncompressed EXIF thumbnail");
|
|
||||||
}
|
|
||||||
|
|
||||||
Entry bitsPerSample = ifd.getEntryById(TIFF.TAG_BITS_PER_SAMPLE);
|
|
||||||
Entry samplesPerPixel = ifd.getEntryById(TIFF.TAG_SAMPLES_PER_PIXEL);
|
|
||||||
Entry photometricInterpretation = ifd.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION);
|
|
||||||
|
|
||||||
// Required
|
|
||||||
int w = ((Number) width.getValue()).intValue();
|
|
||||||
int h = ((Number) height.getValue()).intValue();
|
|
||||||
|
|
||||||
if (bitsPerSample != null) {
|
|
||||||
int[] bpp = (int[]) bitsPerSample.getValue();
|
|
||||||
if (!Arrays.equals(bpp, new int[] {8, 8, 8})) {
|
|
||||||
throw new IIOException("Unknown BitsPerSample value for uncompressed EXIF thumbnail (expected [8, 8, 8]): " + bitsPerSample.getValueAsString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (samplesPerPixel != null && (Integer) samplesPerPixel.getValue() != 3) {
|
|
||||||
throw new IIOException("Unknown SamplesPerPixel value for uncompressed EXIF thumbnail (expected 3): " + samplesPerPixel.getValueAsString());
|
|
||||||
}
|
|
||||||
|
|
||||||
int interpretation = photometricInterpretation != null ? ((Number) photometricInterpretation.getValue()).intValue() : 2;
|
|
||||||
|
|
||||||
// IFD1 should contain strip offsets for uncompressed images
|
|
||||||
Entry offset = ifd.getEntryById(TIFF.TAG_STRIP_OFFSETS);
|
|
||||||
if (offset != null) {
|
|
||||||
stream.seek(((Number) offset.getValue()).longValue());
|
|
||||||
|
|
||||||
// Read raw image data, either RGB or YCbCr
|
|
||||||
int thumbSize = w * h * 3;
|
|
||||||
byte[] thumbData = JPEGImageReader.readFully(stream, thumbSize);
|
|
||||||
|
|
||||||
switch (interpretation) {
|
|
||||||
case 2:
|
|
||||||
// RGB
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
// YCbCr
|
|
||||||
for (int i = 0; i < thumbSize; i += 3) {
|
|
||||||
YCbCrConverter.convertYCbCr2RGB(thumbData, thumbData, i);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IIOException("Unknown PhotometricInterpretation value for uncompressed EXIF thumbnail (expected 2 or 6): " + interpretation);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ThumbnailReader.readRawThumbnail(thumbData, thumbSize, 0, w, h);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new IIOException("Missing StripOffsets tag for uncompressed EXIF thumbnail");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getWidth() throws IOException {
|
|
||||||
if (compression == 1) { // 1 = no compression
|
|
||||||
Entry width = ifd.getEntryById(TIFF.TAG_IMAGE_WIDTH);
|
|
||||||
|
|
||||||
if (width == null) {
|
|
||||||
throw new IIOException("Missing dimensions for uncompressed EXIF thumbnail");
|
|
||||||
}
|
|
||||||
|
|
||||||
return ((Number) width.getValue()).intValue();
|
|
||||||
}
|
|
||||||
else if (compression == 6) { // 6 = JPEG compression
|
|
||||||
return readJPEGCached(false).getWidth();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new IIOException("Unsupported EXIF thumbnail compression (expected 1 or 6): " + compression);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getHeight() throws IOException {
|
|
||||||
if (compression == 1) { // 1 = no compression
|
|
||||||
Entry height = ifd.getEntryById(TIFF.TAG_IMAGE_HEIGHT);
|
|
||||||
|
|
||||||
if (height == null) {
|
|
||||||
throw new IIOException("Missing dimensions for uncompressed EXIF thumbnail");
|
|
||||||
}
|
|
||||||
|
|
||||||
return ((Number) height.getValue()).intValue();
|
|
||||||
}
|
|
||||||
else if (compression == 6) { // 6 = JPEG compression
|
|
||||||
return readJPEGCached(false).getHeight();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new IIOException("Unsupported EXIF thumbnail compression (expected 1 or 6): " + compression);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -38,7 +38,7 @@ import java.io.IOException;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JFIFSegment
|
* A JFIF segment.
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
* @author last modified by $Author: haraldk$
|
* @author last modified by $Author: haraldk$
|
||||||
@ -54,8 +54,8 @@ final class JFIF extends Application {
|
|||||||
final int yThumbnail;
|
final int yThumbnail;
|
||||||
final byte[] thumbnail;
|
final byte[] thumbnail;
|
||||||
|
|
||||||
private JFIF(int majorVersion, int minorVersion, int units, int xDensity, int yDensity, int xThumbnail, int yThumbnail, byte[] thumbnail, byte[] data) {
|
JFIF(int majorVersion, int minorVersion, int units, int xDensity, int yDensity, int xThumbnail, int yThumbnail, byte[] thumbnail) {
|
||||||
super(JPEG.APP0, "JFIF", data);
|
super(JPEG.APP0, "JFIF", new byte[5 + 9 + (thumbnail != null ? thumbnail.length : 0)]);
|
||||||
|
|
||||||
this.majorVersion = majorVersion;
|
this.majorVersion = majorVersion;
|
||||||
this.minorVersion = minorVersion;
|
this.minorVersion = minorVersion;
|
||||||
@ -98,7 +98,7 @@ final class JFIF extends Application {
|
|||||||
throw new EOFException();
|
throw new EOFException();
|
||||||
}
|
}
|
||||||
|
|
||||||
data.readFully(new byte[5]);
|
data.readFully(new byte[5]); // Skip "JFIF\0"
|
||||||
|
|
||||||
byte[] bytes = new byte[length - 2 - 5];
|
byte[] bytes = new byte[length - 2 - 5];
|
||||||
data.readFully(bytes);
|
data.readFully(bytes);
|
||||||
@ -115,8 +115,7 @@ final class JFIF extends Application {
|
|||||||
buffer.getShort() & 0xffff,
|
buffer.getShort() & 0xffff,
|
||||||
x = buffer.get() & 0xff,
|
x = buffer.get() & 0xff,
|
||||||
y = buffer.get() & 0xff,
|
y = buffer.get() & 0xff,
|
||||||
getBytes(buffer, Math.min(buffer.remaining(), x * y * 3)),
|
getBytes(buffer, Math.min(buffer.remaining(), x * y * 3))
|
||||||
bytes
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,17 +30,29 @@
|
|||||||
|
|
||||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
* @author last modified by $Author: haraldk$
|
* @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 {
|
final class JFIFThumbnail {
|
||||||
void thumbnailStarted(int imageIndex, int thumbnailIndex);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
@ -35,7 +35,7 @@ import java.io.IOException;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JFXXSegment
|
* A JFXX segment (aka JFIF extension segment).
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
* @author last modified by $Author: haraldk$
|
* @author last modified by $Author: haraldk$
|
||||||
@ -49,8 +49,8 @@ final class JFXX extends Application {
|
|||||||
final int extensionCode;
|
final int extensionCode;
|
||||||
final byte[] thumbnail;
|
final byte[] thumbnail;
|
||||||
|
|
||||||
private JFXX(final int extensionCode, final byte[] thumbnail, final byte[] data) {
|
JFXX(final int extensionCode, final byte[] thumbnail) {
|
||||||
super(com.twelvemonkeys.imageio.metadata.jpeg.JPEG.APP0, "JFXX", data);
|
super(com.twelvemonkeys.imageio.metadata.jpeg.JPEG.APP0, "JFXX", new byte[1 + (thumbnail != null ? thumbnail.length : 0)]);
|
||||||
|
|
||||||
this.extensionCode = extensionCode;
|
this.extensionCode = extensionCode;
|
||||||
this.thumbnail = thumbnail;
|
this.thumbnail = thumbnail;
|
||||||
@ -82,8 +82,7 @@ final class JFXX extends Application {
|
|||||||
|
|
||||||
return new JFXX(
|
return new JFXX(
|
||||||
bytes[0] & 0xff,
|
bytes[0] & 0xff,
|
||||||
bytes.length - 1 > 0 ? Arrays.copyOfRange(bytes, 1, bytes.length - 1) : null,
|
bytes.length - 1 > 0 ? Arrays.copyOfRange(bytes, 1, bytes.length - 1) : null
|
||||||
bytes
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,178 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2012, Harald Kuhr
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
*
|
|
||||||
* * Redistributions of source code must retain the above copyright notice, this
|
|
||||||
* list of conditions and the following disclaimer.
|
|
||||||
*
|
|
||||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
|
||||||
* and/or other materials provided with the distribution.
|
|
||||||
*
|
|
||||||
* * Neither the name of the copyright holder nor the names of its
|
|
||||||
* contributors may be used to endorse or promote products derived from
|
|
||||||
* this software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
|
||||||
|
|
||||||
import com.twelvemonkeys.image.InverseColorMapIndexColorModel;
|
|
||||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
|
||||||
import com.twelvemonkeys.lang.Validate;
|
|
||||||
|
|
||||||
import javax.imageio.IIOException;
|
|
||||||
import javax.imageio.ImageReader;
|
|
||||||
import javax.imageio.metadata.IIOMetadata;
|
|
||||||
import javax.imageio.stream.ImageInputStream;
|
|
||||||
import java.awt.image.*;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.ref.SoftReference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JFXXThumbnailReader
|
|
||||||
*
|
|
||||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
|
||||||
* @author last modified by $Author: haraldk$
|
|
||||||
* @version $Id: JFXXThumbnailReader.java,v 1.0 18.04.12 12:19 haraldk Exp$
|
|
||||||
*/
|
|
||||||
final class JFXXThumbnailReader extends ThumbnailReader {
|
|
||||||
|
|
||||||
private final ImageReader reader;
|
|
||||||
private final JFXX segment;
|
|
||||||
|
|
||||||
private transient SoftReference<BufferedImage> cachedThumbnail;
|
|
||||||
|
|
||||||
JFXXThumbnailReader(final ThumbnailReadProgressListener progressListener, final ImageReader jpegReader, final int imageIndex, final int thumbnailIndex, final JFXX segment) {
|
|
||||||
super(progressListener, imageIndex, thumbnailIndex);
|
|
||||||
this.reader = Validate.notNull(jpegReader);
|
|
||||||
this.segment = segment;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BufferedImage read() throws IOException {
|
|
||||||
processThumbnailStarted();
|
|
||||||
|
|
||||||
BufferedImage thumbnail;
|
|
||||||
switch (segment.extensionCode) {
|
|
||||||
case JFXX.JPEG:
|
|
||||||
thumbnail = readJPEGCached(true);
|
|
||||||
break;
|
|
||||||
case JFXX.INDEXED:
|
|
||||||
thumbnail = readIndexed();
|
|
||||||
break;
|
|
||||||
case JFXX.RGB:
|
|
||||||
thumbnail = readRGB();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IIOException(String.format("Unsupported JFXX extension code: %d", segment.extensionCode));
|
|
||||||
}
|
|
||||||
|
|
||||||
processThumbnailProgress(100f);
|
|
||||||
processThumbnailComplete();
|
|
||||||
|
|
||||||
return thumbnail;
|
|
||||||
}
|
|
||||||
|
|
||||||
IIOMetadata readMetadata() throws IOException {
|
|
||||||
ImageInputStream input = new ByteArrayImageInputStream(segment.thumbnail);
|
|
||||||
|
|
||||||
try {
|
|
||||||
reader.setInput(input);
|
|
||||||
|
|
||||||
return reader.getImageMetadata(0);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
input.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private BufferedImage readJPEGCached(boolean pixelsExposed) throws IOException {
|
|
||||||
BufferedImage thumbnail = cachedThumbnail != null ? cachedThumbnail.get() : null;
|
|
||||||
|
|
||||||
if (thumbnail == null) {
|
|
||||||
ImageInputStream stream = new ByteArrayImageInputStream(segment.thumbnail);
|
|
||||||
try {
|
|
||||||
thumbnail = readJPEGThumbnail(reader, stream);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
stream.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cachedThumbnail = pixelsExposed ? null : new SoftReference<>(thumbnail);
|
|
||||||
|
|
||||||
return thumbnail;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getWidth() throws IOException {
|
|
||||||
switch (segment.extensionCode) {
|
|
||||||
case JFXX.RGB:
|
|
||||||
case JFXX.INDEXED:
|
|
||||||
return segment.thumbnail[0] & 0xff;
|
|
||||||
case JFXX.JPEG:
|
|
||||||
return readJPEGCached(false).getWidth();
|
|
||||||
default:
|
|
||||||
throw new IIOException(String.format("Unsupported JFXX extension code: %d", segment.extensionCode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getHeight() throws IOException {
|
|
||||||
switch (segment.extensionCode) {
|
|
||||||
case JFXX.RGB:
|
|
||||||
case JFXX.INDEXED:
|
|
||||||
return segment.thumbnail[1] & 0xff;
|
|
||||||
case JFXX.JPEG:
|
|
||||||
return readJPEGCached(false).getHeight();
|
|
||||||
default:
|
|
||||||
throw new IIOException(String.format("Unsupported JFXX extension code: %d", segment.extensionCode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private BufferedImage readIndexed() {
|
|
||||||
// 1 byte: xThumb
|
|
||||||
// 1 byte: yThumb
|
|
||||||
// 768 bytes: palette
|
|
||||||
// x * y bytes: 8 bit indexed pixels
|
|
||||||
int w = segment.thumbnail[0] & 0xff;
|
|
||||||
int h = segment.thumbnail[1] & 0xff;
|
|
||||||
|
|
||||||
int[] rgbs = new int[256];
|
|
||||||
for (int i = 0; i < rgbs.length; i++) {
|
|
||||||
rgbs[i] = (segment.thumbnail[3 * i + 2] & 0xff) << 16
|
|
||||||
| (segment.thumbnail[3 * i + 3] & 0xff) << 8
|
|
||||||
| (segment.thumbnail[3 * i + 4] & 0xff);
|
|
||||||
}
|
|
||||||
|
|
||||||
IndexColorModel icm = new InverseColorMapIndexColorModel(8, rgbs.length, rgbs, 0, false, -1, DataBuffer.TYPE_BYTE);
|
|
||||||
DataBufferByte buffer = new DataBufferByte(segment.thumbnail, segment.thumbnail.length - 770, 770);
|
|
||||||
WritableRaster raster = Raster.createPackedRaster(buffer, w, h, 8, null);
|
|
||||||
|
|
||||||
return new BufferedImage(icm, raster, icm.isAlphaPremultiplied(), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private BufferedImage readRGB() {
|
|
||||||
// 1 byte: xThumb
|
|
||||||
// 1 byte: yThumb
|
|
||||||
// 3 * x * y bytes: 24 bit RGB pixels
|
|
||||||
int w = segment.thumbnail[0] & 0xff;
|
|
||||||
int h = segment.thumbnail[1] & 0xff;
|
|
||||||
|
|
||||||
return ThumbnailReader.readRawThumbnail(segment.thumbnail, segment.thumbnail.length - 2, 2, w, h);
|
|
||||||
}
|
|
||||||
}
|
|
@ -32,6 +32,7 @@ package com.twelvemonkeys.imageio.plugins.jpeg;
|
|||||||
|
|
||||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
||||||
import com.twelvemonkeys.xml.XMLSerializer;
|
import com.twelvemonkeys.xml.XMLSerializer;
|
||||||
|
|
||||||
import org.w3c.dom.Element;
|
import org.w3c.dom.Element;
|
||||||
import org.w3c.dom.Node;
|
import org.w3c.dom.Node;
|
||||||
import org.w3c.dom.NodeList;
|
import org.w3c.dom.NodeList;
|
||||||
@ -132,7 +133,7 @@ final class JPEGImage10MetadataCleaner {
|
|||||||
IIOMetadataNode app0JFXX = new IIOMetadataNode("app0JFXX");
|
IIOMetadataNode app0JFXX = new IIOMetadataNode("app0JFXX");
|
||||||
app0JFXX.setAttribute("extensionCode", String.valueOf(jfxx.extensionCode));
|
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;
|
IIOMetadataNode jfifThumb;
|
||||||
|
|
||||||
switch (jfxx.extensionCode) {
|
switch (jfxx.extensionCode) {
|
||||||
|
@ -34,14 +34,10 @@ import com.twelvemonkeys.imageio.ImageReaderBase;
|
|||||||
import com.twelvemonkeys.imageio.color.ColorSpaces;
|
import com.twelvemonkeys.imageio.color.ColorSpaces;
|
||||||
import com.twelvemonkeys.imageio.color.YCbCrConverter;
|
import com.twelvemonkeys.imageio.color.YCbCrConverter;
|
||||||
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
|
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.JPEG;
|
||||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
||||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
|
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.metadata.tiff.TIFFReader;
|
||||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
|
||||||
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
|
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
|
||||||
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
|
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
|
||||||
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
|
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.color.ICC_Profile;
|
||||||
import java.awt.image.*;
|
import java.awt.image.*;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@ -667,7 +662,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
|||||||
private void initDelegate(boolean seekForwardOnly, boolean ignoreMetadata) throws IOException {
|
private void initDelegate(boolean seekForwardOnly, boolean ignoreMetadata) throws IOException {
|
||||||
// JPEGSegmentImageInputStream that filters out/skips bad/unnecessary segments
|
// JPEGSegmentImageInputStream that filters out/skips bad/unnecessary segments
|
||||||
delegate.setInput(imageInput != null
|
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);
|
: null, seekForwardOnly, ignoreMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -705,6 +700,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initHeader(final int imageIndex) throws IOException {
|
private void initHeader(final int imageIndex) throws IOException {
|
||||||
|
assertInput();
|
||||||
if (imageIndex < 0) {
|
if (imageIndex < 0) {
|
||||||
throw new IndexOutOfBoundsException("imageIndex < 0: " + imageIndex);
|
throw new IndexOutOfBoundsException("imageIndex < 0: " + imageIndex);
|
||||||
}
|
}
|
||||||
@ -889,25 +885,25 @@ public final class JPEGImageReader extends ImageReaderBase {
|
|||||||
return jfxx.isEmpty() ? null : (JFXX) jfxx.get(0);
|
return jfxx.isEmpty() ? null : (JFXX) jfxx.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompoundDirectory getExif() throws IOException {
|
private EXIF getExif() throws IOException {
|
||||||
List<Application> exifSegments = getAppSegments(JPEG.APP1, "Exif");
|
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()) {
|
private CompoundDirectory parseExif(final EXIF exif) throws IOException {
|
||||||
Application exif = exifSegments.get(0);
|
if (exif != null) {
|
||||||
int offset = exif.identifier.length() + 2; // Incl. pad
|
// Identifier is "Exif\0" + 1 byte pad
|
||||||
|
if (exif.data.length > exif.identifier.length() + 2) {
|
||||||
if (exif.data.length <= offset) {
|
try (ImageInputStream stream = exif.exifData()) {
|
||||||
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)) {
|
|
||||||
return (CompoundDirectory) new TIFFReader().read(stream);
|
return (CompoundDirectory) new TIFFReader().read(stream);
|
||||||
}
|
}
|
||||||
catch (IIOException e) {
|
catch (IIOException e) {
|
||||||
processWarningOccurred("Exif chunk is present, but can't be read: " + e.getMessage());
|
processWarningOccurred("Exif chunk is present, but can't be read: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
processWarningOccurred("Exif chunk has no data.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -916,7 +912,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
|||||||
// TODO: Util method?
|
// TODO: Util method?
|
||||||
static byte[] readFully(DataInput stream, int len) throws IOException {
|
static byte[] readFully(DataInput stream, int len) throws IOException {
|
||||||
if (len == 0) {
|
if (len == 0) {
|
||||||
return null;
|
throw new IllegalArgumentException("len == 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] data = new byte[len];
|
byte[] data = new byte[len];
|
||||||
@ -1089,109 +1085,26 @@ public final class JPEGImageReader extends ImageReaderBase {
|
|||||||
|
|
||||||
if (thumbnails == null) {
|
if (thumbnails == null) {
|
||||||
thumbnails = new ArrayList<>();
|
thumbnails = new ArrayList<>();
|
||||||
ThumbnailReadProgressListener thumbnailProgressDelegator = new ThumbnailProgressDelegate();
|
|
||||||
|
JPEGSegmentWarningDelegate listenerDelegate = new JPEGSegmentWarningDelegate();
|
||||||
|
|
||||||
// Read JFIF thumbnails if present
|
// Read JFIF thumbnails if present
|
||||||
JFIF jfif = getJFIF();
|
ThumbnailReader thumbnailReader = JFIFThumbnail.from(getJFIF(), listenerDelegate);
|
||||||
if (jfif != null && jfif.thumbnail != null) {
|
if (thumbnailReader != null) {
|
||||||
// TODO: Check if the JFIF segment really has room for this thumbnail?
|
thumbnails.add(thumbnailReader);
|
||||||
thumbnails.add(new JFIFThumbnailReader(thumbnailProgressDelegator, imageIndex, thumbnails.size(), jfif));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read JFXX thumbnails if present
|
// Read JFXX thumbnails if present
|
||||||
JFXX jfxx = getJFXX();
|
thumbnailReader = JFXXThumbnail.from(getJFXX(), getThumbnailReader(), listenerDelegate);
|
||||||
if (jfxx != null && jfxx.thumbnail != null) {
|
if (thumbnailReader != null) {
|
||||||
switch (jfxx.extensionCode) {
|
thumbnails.add(thumbnailReader);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read Exif thumbnails if present
|
// Read Exif thumbnails if present
|
||||||
List<Application> exifSegments = getAppSegments(JPEG.APP1, "Exif");
|
EXIF exif = getExif();
|
||||||
if (!exifSegments.isEmpty()) {
|
thumbnailReader = EXIFThumbnail.from(exif, parseExif(exif), getThumbnailReader(), listenerDelegate);
|
||||||
Application exif = exifSegments.get(0);
|
if (thumbnailReader != null) {
|
||||||
|
thumbnails.add(thumbnailReader);
|
||||||
// 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1234,13 +1147,13 @@ public final class JPEGImageReader extends ImageReaderBase {
|
|||||||
public BufferedImage readThumbnail(int imageIndex, int thumbnailIndex) throws IOException {
|
public BufferedImage readThumbnail(int imageIndex, int thumbnailIndex) throws IOException {
|
||||||
checkThumbnailBounds(imageIndex, thumbnailIndex);
|
checkThumbnailBounds(imageIndex, thumbnailIndex);
|
||||||
|
|
||||||
// processThumbnailStarted(imageIndex, thumbnailIndex);
|
processThumbnailStarted(imageIndex, thumbnailIndex);
|
||||||
// processThumbnailProgress(0f);
|
processThumbnailProgress(0f);
|
||||||
|
|
||||||
BufferedImage thumbnail = thumbnails.get(thumbnailIndex).read();;
|
BufferedImage thumbnail = thumbnails.get(thumbnailIndex).read();;
|
||||||
|
|
||||||
// processThumbnailProgress(100f);
|
processThumbnailProgress(100f);
|
||||||
// processThumbnailComplete();
|
processThumbnailComplete();
|
||||||
|
|
||||||
return thumbnail;
|
return thumbnail;
|
||||||
}
|
}
|
||||||
@ -1251,7 +1164,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
|||||||
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
|
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
|
||||||
initHeader(imageIndex);
|
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
|
@Override
|
||||||
@ -1376,24 +1289,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ThumbnailProgressDelegate implements ThumbnailReadProgressListener {
|
private class JPEGSegmentWarningDelegate implements JPEGSegmentWarningListener {
|
||||||
@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 {
|
|
||||||
@Override
|
@Override
|
||||||
public void warningOccurred(String warning) {
|
public void warningOccurred(String warning) {
|
||||||
processWarningOccurred(warning);
|
processWarningOccurred(warning);
|
||||||
|
@ -59,7 +59,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
|
|||||||
// TODO: Support multiple JPEG streams (SOI...EOI, SOI...EOI, ...) in a single file
|
// TODO: Support multiple JPEG streams (SOI...EOI, SOI...EOI, ...) in a single file
|
||||||
|
|
||||||
private final ImageInputStream stream;
|
private final ImageInputStream stream;
|
||||||
private final JPEGSegmentStreamWarningListener warningListener;
|
private final JPEGSegmentWarningListener warningListener;
|
||||||
|
|
||||||
private final ComponentIdSet componentIds = new ComponentIdSet();
|
private final ComponentIdSet componentIds = new ComponentIdSet();
|
||||||
|
|
||||||
@ -68,13 +68,13 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
|
|||||||
private Segment segment;
|
private Segment segment;
|
||||||
|
|
||||||
|
|
||||||
JPEGSegmentImageInputStream(final ImageInputStream stream, final JPEGSegmentStreamWarningListener warningListener) {
|
JPEGSegmentImageInputStream(final ImageInputStream stream, final JPEGSegmentWarningListener warningListener) {
|
||||||
this.stream = notNull(stream, "stream");
|
this.stream = notNull(stream, "stream");
|
||||||
this.warningListener = notNull(warningListener, "warningListener");
|
this.warningListener = notNull(warningListener, "warningListener");
|
||||||
}
|
}
|
||||||
|
|
||||||
JPEGSegmentImageInputStream(final ImageInputStream stream) {
|
JPEGSegmentImageInputStream(final ImageInputStream stream) {
|
||||||
this(stream, JPEGSegmentStreamWarningListener.NULL_LISTENER);
|
this(stream, JPEGSegmentWarningListener.NULL_LISTENER);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processWarningOccured(final String warning) {
|
private void processWarningOccured(final String warning) {
|
||||||
|
@ -33,10 +33,10 @@ package com.twelvemonkeys.imageio.plugins.jpeg;
|
|||||||
/**
|
/**
|
||||||
* JPEGSegmentStreamWarningListener
|
* JPEGSegmentStreamWarningListener
|
||||||
*/
|
*/
|
||||||
interface JPEGSegmentStreamWarningListener {
|
interface JPEGSegmentWarningListener {
|
||||||
void warningOccurred(String warning);
|
void warningOccurred(String warning);
|
||||||
|
|
||||||
JPEGSegmentStreamWarningListener NULL_LISTENER = new JPEGSegmentStreamWarningListener() {
|
JPEGSegmentWarningListener NULL_LISTENER = new JPEGSegmentWarningListener() {
|
||||||
@Override
|
@Override
|
||||||
public void warningOccurred(final String warning) {}
|
public void warningOccurred(final String warning) {}
|
||||||
};
|
};
|
@ -31,12 +31,16 @@
|
|||||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||||
|
|
||||||
import javax.imageio.ImageReader;
|
import javax.imageio.ImageReader;
|
||||||
|
import javax.imageio.metadata.IIOMetadata;
|
||||||
import javax.imageio.stream.ImageInputStream;
|
import javax.imageio.stream.ImageInputStream;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.color.ColorSpace;
|
import java.awt.color.ColorSpace;
|
||||||
import java.awt.image.*;
|
import java.awt.image.*;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static com.twelvemonkeys.lang.Validate.isTrue;
|
||||||
|
import static com.twelvemonkeys.lang.Validate.notNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ThumbnailReader
|
* ThumbnailReader
|
||||||
*
|
*
|
||||||
@ -46,68 +50,156 @@ import java.io.IOException;
|
|||||||
*/
|
*/
|
||||||
abstract class ThumbnailReader {
|
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 BufferedImage read() throws IOException;
|
||||||
|
|
||||||
public abstract int getWidth() throws IOException;
|
public abstract int getWidth() throws IOException;
|
||||||
|
|
||||||
public abstract int getHeight() throws IOException;
|
public abstract int getHeight() throws IOException;
|
||||||
|
|
||||||
private static class NullProgressListener implements ThumbnailReadProgressListener {
|
public IIOMetadata readMetadata() throws IOException {
|
||||||
@Override
|
return null;
|
||||||
public void thumbnailStarted(int imageIndex, int thumbnailIndex) {
|
}
|
||||||
|
|
||||||
|
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
|
@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
|
@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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,7 @@ import java.io.IOException;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AbstractThumbnailReaderTest
|
* AbstractThumbnailReaderTest
|
||||||
@ -52,9 +53,9 @@ public abstract class AbstractThumbnailReaderTest {
|
|||||||
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
|
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract ThumbnailReader createReader(
|
protected final JPEGSegmentWarningListener listener = mock(JPEGSegmentWarningListener.class);
|
||||||
ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, ImageInputStream stream
|
|
||||||
) throws IOException;
|
protected abstract ThumbnailReader createReader(ImageInputStream stream) throws IOException;
|
||||||
|
|
||||||
protected final ImageInputStream createStream(final String name) throws IOException {
|
protected final ImageInputStream createStream(final String name) throws IOException {
|
||||||
URL resource = getClass().getResource(name);
|
URL resource = getClass().getResource(name);
|
||||||
|
@ -35,18 +35,19 @@ import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
|||||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
||||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
|
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
|
||||||
import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
|
import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.InOrder;
|
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
|
import javax.imageio.ImageReader;
|
||||||
import javax.imageio.stream.ImageInputStream;
|
import javax.imageio.stream.ImageInputStream;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.DataInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EXIFThumbnailReaderTest
|
* EXIFThumbnailReaderTest
|
||||||
@ -57,31 +58,28 @@ import static org.mockito.Mockito.*;
|
|||||||
*/
|
*/
|
||||||
public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||||
|
|
||||||
|
private final ImageReader thumbnailReader = ImageIO.getImageReadersByFormatName("jpeg").next();
|
||||||
|
|
||||||
@Override
|
@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");
|
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP1, "Exif");
|
||||||
stream.close();
|
stream.close();
|
||||||
|
|
||||||
assertNotNull(segments);
|
assertNotNull(segments);
|
||||||
assertFalse(segments.isEmpty());
|
assertFalse(segments.isEmpty());
|
||||||
|
|
||||||
TIFFReader reader = new TIFFReader();
|
JPEGSegment exifSegment = segments.get(0);
|
||||||
InputStream data = segments.get(0).data();
|
InputStream data = exifSegment.segmentData();
|
||||||
if (data.read() < 0) {
|
byte[] exifData = new byte[exifSegment.segmentLength() - 2];
|
||||||
throw new AssertionError("EOF!");
|
new DataInputStream(data).readFully(exifData);
|
||||||
}
|
|
||||||
|
|
||||||
ImageInputStream exifStream = ImageIO.createImageInputStream(data);
|
EXIF exif = new EXIF(exifData);
|
||||||
CompoundDirectory ifds = (CompoundDirectory) reader.read(exifStream);
|
return EXIFThumbnail.from(exif, (CompoundDirectory) new TIFFReader().read(exif.exifData()), thumbnailReader, listener);
|
||||||
|
|
||||||
assertEquals(2, ifds.directoryCount());
|
|
||||||
|
|
||||||
return new EXIFThumbnailReader(progressListener, ImageIO.getImageReadersByFormatName("JPEG").next(), imageIndex, thumbnailIndex, ifds.getDirectory(1), exifStream);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadJPEG() throws IOException {
|
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(114, reader.getWidth());
|
||||||
assertEquals(160, reader.getHeight());
|
assertEquals(160, reader.getHeight());
|
||||||
@ -94,7 +92,7 @@ public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadRaw() throws IOException {
|
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(80, reader.getWidth());
|
||||||
assertEquals(60, reader.getHeight());
|
assertEquals(60, reader.getHeight());
|
||||||
@ -104,28 +102,4 @@ public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
|||||||
assertEquals(80, thumbnail.getWidth());
|
assertEquals(80, thumbnail.getWidth());
|
||||||
assertEquals(60, thumbnail.getHeight());
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -33,8 +33,8 @@ package com.twelvemonkeys.imageio.plugins.jpeg;
|
|||||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
||||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
||||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
|
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.InOrder;
|
|
||||||
|
|
||||||
import javax.imageio.stream.ImageInputStream;
|
import javax.imageio.stream.ImageInputStream;
|
||||||
import java.awt.image.BufferedImage;
|
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$
|
* @version $Id: JFIFThumbnailReaderTest.java,v 1.0 04.05.12 15:56 haraldk Exp$
|
||||||
*/
|
*/
|
||||||
public class JFIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
public class JFIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||||
|
|
||||||
@Override
|
@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");
|
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP0, "JFIF");
|
||||||
stream.close();
|
stream.close();
|
||||||
|
|
||||||
@ -62,12 +63,54 @@ public class JFIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
|||||||
assertFalse(segments.isEmpty());
|
assertFalse(segments.isEmpty());
|
||||||
|
|
||||||
JPEGSegment segment = segments.get(0);
|
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
|
@Test
|
||||||
public void testReadRaw() throws IOException {
|
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(131, reader.getWidth());
|
||||||
assertEquals(122, reader.getHeight());
|
assertEquals(122, reader.getHeight());
|
||||||
@ -80,7 +123,7 @@ public class JFIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadNonSpecGray() throws IOException {
|
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(127, reader.getWidth());
|
||||||
assertEquals(76, reader.getHeight());
|
assertEquals(76, reader.getHeight());
|
||||||
@ -91,16 +134,4 @@ public class JFIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
|||||||
assertEquals(127, thumbnail.getWidth());
|
assertEquals(127, thumbnail.getWidth());
|
||||||
assertEquals(76, thumbnail.getHeight());
|
assertEquals(76, thumbnail.getHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testProgressListenerRaw() throws IOException {
|
|
||||||
ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class);
|
|
||||||
|
|
||||||
createReader(listener, 0, 99, createStream("/jpeg/jfif-jfif-and-exif-thumbnail-sharpshot-iphone.jpg")).read();
|
|
||||||
|
|
||||||
InOrder order = inOrder(listener);
|
|
||||||
order.verify(listener).thumbnailStarted(0, 99);
|
|
||||||
order.verify(listener, atLeastOnce()).thumbnailProgress(100f);
|
|
||||||
order.verify(listener).thumbnailComplete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -33,10 +33,12 @@ package com.twelvemonkeys.imageio.plugins.jpeg;
|
|||||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
||||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
||||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
|
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.InOrder;
|
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
|
import javax.imageio.ImageReader;
|
||||||
import javax.imageio.stream.ImageInputStream;
|
import javax.imageio.stream.ImageInputStream;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
@ -44,6 +46,7 @@ import java.io.IOException;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
import static org.mockito.Matchers.anyString;
|
||||||
import static org.mockito.Mockito.*;
|
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$
|
* @version $Id: JFXXThumbnailReaderTest.java,v 1.0 04.05.12 15:56 haraldk Exp$
|
||||||
*/
|
*/
|
||||||
public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||||
|
private final ImageReader thumbnailReader = ImageIO.getImageReadersByFormatName("jpeg").next();
|
||||||
|
|
||||||
@Override
|
@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");
|
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP0, "JFXX");
|
||||||
stream.close();
|
stream.close();
|
||||||
|
|
||||||
@ -63,12 +68,81 @@ public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
|||||||
assertFalse(segments.isEmpty());
|
assertFalse(segments.isEmpty());
|
||||||
|
|
||||||
JPEGSegment jfxx = segments.get(0);
|
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
|
@Test
|
||||||
public void testReadJPEG() throws IOException {
|
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(80, reader.getWidth());
|
||||||
assertEquals(60, reader.getHeight());
|
assertEquals(60, reader.getHeight());
|
||||||
@ -81,16 +155,4 @@ public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
|||||||
|
|
||||||
// TODO: Test JFXX indexed thumbnail
|
// TODO: Test JFXX indexed thumbnail
|
||||||
// TODO: Test JFXX RGB 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -441,6 +441,7 @@ final class TGAImageReader extends ImageReaderBase {
|
|||||||
WritableRaster rowRaster = rawType.createBufferedImage(width, 1).getRaster();
|
WritableRaster rowRaster = rawType.createBufferedImage(width, 1).getRaster();
|
||||||
|
|
||||||
processThumbnailStarted(imageIndex, thumbnailIndex);
|
processThumbnailStarted(imageIndex, thumbnailIndex);
|
||||||
|
processThumbnailProgress(0f);
|
||||||
|
|
||||||
// Thumbnail is always stored non-compressed, no need for RLE support
|
// Thumbnail is always stored non-compressed, no need for RLE support
|
||||||
imageInput.seek(extensions.getThumbnailOffset() + 2);
|
imageInput.seek(extensions.getThumbnailOffset() + 2);
|
||||||
@ -468,6 +469,7 @@ final class TGAImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
processThumbnailProgress(100f);
|
||||||
processThumbnailComplete();
|
processThumbnailComplete();
|
||||||
|
|
||||||
return destination;
|
return destination;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user