mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-05 04:25:29 -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();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadNoInput() throws IOException {
|
||||
ImageReader reader = createReader();
|
||||
// Do not set input
|
||||
|
||||
BufferedImage image = null;
|
||||
try {
|
||||
image = reader.read(0);
|
||||
fail("Read image with no input");
|
||||
}
|
||||
catch (IllegalStateException ignore) {
|
||||
}
|
||||
catch (IOException e) {
|
||||
failBecause("Image could not be read", e);
|
||||
}
|
||||
assertNull(image);
|
||||
|
||||
reader.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReRead() throws IOException {
|
||||
ImageReader reader = createReader();
|
||||
@ -323,69 +303,71 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
|
||||
reader.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void testReadNoInput() throws IOException {
|
||||
ImageReader reader = createReader();
|
||||
// Do not set input
|
||||
|
||||
try {
|
||||
reader.read(0);
|
||||
fail("Read image with no input");
|
||||
}
|
||||
catch (IOException e) {
|
||||
failBecause("Image could not be read", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IndexOutOfBoundsException.class)
|
||||
public void testReadIndexNegativeWithParam() throws IOException {
|
||||
ImageReader reader = createReader();
|
||||
TestData data = getTestData().get(0);
|
||||
reader.setInput(data.getInputStream());
|
||||
|
||||
BufferedImage image = null;
|
||||
try {
|
||||
image = reader.read(-1, reader.getDefaultReadParam());
|
||||
reader.read(-1, reader.getDefaultReadParam());
|
||||
fail("Read image with illegal index");
|
||||
}
|
||||
catch (IndexOutOfBoundsException ignore) {
|
||||
}
|
||||
catch (IOException e) {
|
||||
failBecause("Image could not be read", e);
|
||||
}
|
||||
|
||||
assertNull(image);
|
||||
|
||||
reader.dispose();
|
||||
finally {
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = IndexOutOfBoundsException.class)
|
||||
public void testReadIndexOutOfBoundsWithParam() throws IOException {
|
||||
ImageReader reader = createReader();
|
||||
TestData data = getTestData().get(0);
|
||||
reader.setInput(data.getInputStream());
|
||||
|
||||
BufferedImage image = null;
|
||||
try {
|
||||
image = reader.read(Short.MAX_VALUE, reader.getDefaultReadParam());
|
||||
reader.read(Short.MAX_VALUE, reader.getDefaultReadParam());
|
||||
fail("Read image with index out of bounds");
|
||||
}
|
||||
catch (IndexOutOfBoundsException ignore) {
|
||||
}
|
||||
catch (IOException e) {
|
||||
failBecause("Image could not be read", e);
|
||||
}
|
||||
|
||||
assertNull(image);
|
||||
|
||||
reader.dispose();
|
||||
finally {
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void testReadNoInputWithParam() throws IOException {
|
||||
ImageReader reader = createReader();
|
||||
// Do not set input
|
||||
|
||||
BufferedImage image = null;
|
||||
try {
|
||||
image = reader.read(0, reader.getDefaultReadParam());
|
||||
reader.read(0, reader.getDefaultReadParam());
|
||||
fail("Read image with no input");
|
||||
}
|
||||
catch (IllegalStateException ignore) {
|
||||
}
|
||||
catch (IOException e) {
|
||||
failBecause("Image could not be read", e);
|
||||
}
|
||||
|
||||
assertNull(image);
|
||||
|
||||
reader.dispose();
|
||||
finally {
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -1658,6 +1640,64 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
|
||||
reader.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadThumbnails() throws IOException {
|
||||
T reader = createReader();
|
||||
|
||||
if (reader.readerSupportsThumbnails()) {
|
||||
for (TestData testData : getTestData()) {
|
||||
try (ImageInputStream inputStream = testData.getInputStream()) {
|
||||
reader.setInput(inputStream);
|
||||
|
||||
int numImages = reader.getNumImages(true);
|
||||
|
||||
for (int i = 0; i < numImages; i++) {
|
||||
int numThumbnails = reader.getNumThumbnails(0);
|
||||
|
||||
for (int t = 0; t < numThumbnails; t++) {
|
||||
BufferedImage thumbnail = reader.readThumbnail(0, t);
|
||||
|
||||
assertNotNull(thumbnail);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThumbnailProgress() throws IOException {
|
||||
T reader = createReader();
|
||||
|
||||
IIOReadProgressListener listener = mock(IIOReadProgressListener.class);
|
||||
reader.addIIOReadProgressListener(listener);
|
||||
|
||||
if (reader.readerSupportsThumbnails()) {
|
||||
for (TestData testData : getTestData()) {
|
||||
try (ImageInputStream inputStream = testData.getInputStream()) {
|
||||
|
||||
reader.setInput(inputStream);
|
||||
|
||||
int numThumbnails = reader.getNumThumbnails(0);
|
||||
for (int i = 0; i < numThumbnails; i++) {
|
||||
reset(listener);
|
||||
|
||||
reader.readThumbnail(0, i);
|
||||
|
||||
InOrder order = inOrder(listener);
|
||||
order.verify(listener).thumbnailStarted(reader, 0, i);
|
||||
order.verify(listener, atLeastOnce()).thumbnailProgress(reader, 100f);
|
||||
order.verify(listener).thumbnailComplete(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotBadCachingThumbnails() throws IOException {
|
||||
T reader = createReader();
|
||||
|
@ -38,7 +38,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Application.
|
||||
* An application (APPn) segment in the JPEG stream.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
@ -78,7 +78,9 @@ class Application extends Segment {
|
||||
if ("JFXX".equals(identifier)) {
|
||||
return JFXX.read(data, length);
|
||||
}
|
||||
// TODO: Exif?
|
||||
if ("Exif".equals(identifier)) {
|
||||
return EXIF.read(data, length);
|
||||
}
|
||||
case JPEG.APP2:
|
||||
// ICC_PROFILE
|
||||
if ("ICC_PROFILE".equals(identifier)) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2012, Harald Kuhr
|
||||
* Copyright (c) 2021, Harald Kuhr
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
@ -30,41 +30,45 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
||||
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.io.DataInput;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* JFIFThumbnailReader
|
||||
* An EXIF segment.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: JFIFThumbnailReader.java,v 1.0 18.04.12 12:19 haraldk Exp$
|
||||
* @version $Id: JFIFSegment.java,v 1.0 23.04.12 16:52 haraldk Exp$
|
||||
*/
|
||||
final class JFIFThumbnailReader extends ThumbnailReader {
|
||||
private final JFIF segment;
|
||||
|
||||
JFIFThumbnailReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex, final JFIF segment) {
|
||||
super(progressListener, imageIndex, thumbnailIndex);
|
||||
this.segment = segment;
|
||||
final class EXIF extends Application {
|
||||
EXIF(byte[] data) {
|
||||
super(JPEG.APP1, "Exif", data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedImage read() {
|
||||
processThumbnailStarted();
|
||||
BufferedImage thumbnail = readRawThumbnail(segment.thumbnail, segment.thumbnail.length, 0, segment.xThumbnail, segment.yThumbnail);
|
||||
processThumbnailProgress(100f);
|
||||
processThumbnailComplete();
|
||||
|
||||
return thumbnail;
|
||||
public String toString() {
|
||||
return String.format("APP1/Exif, length: %d", data.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() throws IOException {
|
||||
return segment.xThumbnail;
|
||||
ImageInputStream exifData() {
|
||||
// Identifier is "Exif\0" + 1 byte pad
|
||||
int offset = identifier.length() + 2;
|
||||
return new ByteArrayImageInputStream(data, offset, data.length - offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() throws IOException {
|
||||
return segment.yThumbnail;
|
||||
public static EXIF read(final DataInput data, int length) throws IOException {
|
||||
if (length < 2 + 6) {
|
||||
throw new EOFException();
|
||||
}
|
||||
|
||||
byte[] bytes = new byte[length - 2];
|
||||
data.readFully(bytes);
|
||||
|
||||
return new EXIF(bytes);
|
||||
}
|
||||
}
|
@ -0,0 +1,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;
|
||||
|
||||
/**
|
||||
* JFIFSegment
|
||||
* A JFIF segment.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
@ -54,8 +54,8 @@ final class JFIF extends Application {
|
||||
final int yThumbnail;
|
||||
final byte[] thumbnail;
|
||||
|
||||
private JFIF(int majorVersion, int minorVersion, int units, int xDensity, int yDensity, int xThumbnail, int yThumbnail, byte[] thumbnail, byte[] data) {
|
||||
super(JPEG.APP0, "JFIF", data);
|
||||
JFIF(int majorVersion, int minorVersion, int units, int xDensity, int yDensity, int xThumbnail, int yThumbnail, byte[] thumbnail) {
|
||||
super(JPEG.APP0, "JFIF", new byte[5 + 9 + (thumbnail != null ? thumbnail.length : 0)]);
|
||||
|
||||
this.majorVersion = majorVersion;
|
||||
this.minorVersion = minorVersion;
|
||||
@ -98,7 +98,7 @@ final class JFIF extends Application {
|
||||
throw new EOFException();
|
||||
}
|
||||
|
||||
data.readFully(new byte[5]);
|
||||
data.readFully(new byte[5]); // Skip "JFIF\0"
|
||||
|
||||
byte[] bytes = new byte[length - 2 - 5];
|
||||
data.readFully(bytes);
|
||||
@ -115,8 +115,7 @@ final class JFIF extends Application {
|
||||
buffer.getShort() & 0xffff,
|
||||
x = buffer.get() & 0xff,
|
||||
y = buffer.get() & 0xff,
|
||||
getBytes(buffer, Math.min(buffer.remaining(), x * y * 3)),
|
||||
bytes
|
||||
getBytes(buffer, Math.min(buffer.remaining(), x * y * 3))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -30,17 +30,29 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
import com.twelvemonkeys.imageio.plugins.jpeg.ThumbnailReader.UncompressedThumbnailReader;
|
||||
|
||||
/**
|
||||
* ThumbnailReadProgressListener
|
||||
* JFIFThumbnail
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: ThumbnailReadProgressListener.java,v 1.0 07.05.12 10:15 haraldk Exp$
|
||||
* @version $Id: JFIFThumbnail.java,v 1.0 18.04.12 12:19 haraldk Exp$
|
||||
*/
|
||||
interface ThumbnailReadProgressListener {
|
||||
void thumbnailStarted(int imageIndex, int thumbnailIndex);
|
||||
final class JFIFThumbnail {
|
||||
private JFIFThumbnail() {
|
||||
}
|
||||
|
||||
void thumbnailProgress(float percentageDone);
|
||||
static ThumbnailReader from(final JFIF segment, final JPEGSegmentWarningListener listener) {
|
||||
if (segment != null && segment.xThumbnail > 0 && segment.yThumbnail > 0) {
|
||||
if (segment.thumbnail == null || segment.thumbnail.length < segment.xThumbnail * segment.yThumbnail) {
|
||||
listener.warningOccurred("Ignoring truncated JFIF thumbnail");
|
||||
}
|
||||
else {
|
||||
return new UncompressedThumbnailReader(segment.xThumbnail, segment.yThumbnail, segment.thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
void thumbnailComplete();
|
||||
return null;
|
||||
}
|
||||
}
|
@ -35,7 +35,7 @@ import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* JFXXSegment
|
||||
* A JFXX segment (aka JFIF extension segment).
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
@ -49,8 +49,8 @@ final class JFXX extends Application {
|
||||
final int extensionCode;
|
||||
final byte[] thumbnail;
|
||||
|
||||
private JFXX(final int extensionCode, final byte[] thumbnail, final byte[] data) {
|
||||
super(com.twelvemonkeys.imageio.metadata.jpeg.JPEG.APP0, "JFXX", data);
|
||||
JFXX(final int extensionCode, final byte[] thumbnail) {
|
||||
super(com.twelvemonkeys.imageio.metadata.jpeg.JPEG.APP0, "JFXX", new byte[1 + (thumbnail != null ? thumbnail.length : 0)]);
|
||||
|
||||
this.extensionCode = extensionCode;
|
||||
this.thumbnail = thumbnail;
|
||||
@ -82,8 +82,7 @@ final class JFXX extends Application {
|
||||
|
||||
return new JFXX(
|
||||
bytes[0] & 0xff,
|
||||
bytes.length - 1 > 0 ? Arrays.copyOfRange(bytes, 1, bytes.length - 1) : null,
|
||||
bytes
|
||||
bytes.length - 1 > 0 ? Arrays.copyOfRange(bytes, 1, bytes.length - 1) : null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,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.xml.XMLSerializer;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
@ -132,7 +133,7 @@ final class JPEGImage10MetadataCleaner {
|
||||
IIOMetadataNode app0JFXX = new IIOMetadataNode("app0JFXX");
|
||||
app0JFXX.setAttribute("extensionCode", String.valueOf(jfxx.extensionCode));
|
||||
|
||||
JFXXThumbnailReader thumbnailReader = new JFXXThumbnailReader(null, reader.getThumbnailReader(), 0, 0, jfxx);
|
||||
ThumbnailReader thumbnailReader = JFXXThumbnail.from(jfxx, reader.getThumbnailReader(), JPEGSegmentWarningListener.NULL_LISTENER);
|
||||
IIOMetadataNode jfifThumb;
|
||||
|
||||
switch (jfxx.extensionCode) {
|
||||
|
@ -34,14 +34,10 @@ import com.twelvemonkeys.imageio.ImageReaderBase;
|
||||
import com.twelvemonkeys.imageio.color.ColorSpaces;
|
||||
import com.twelvemonkeys.imageio.color.YCbCrConverter;
|
||||
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
|
||||
import com.twelvemonkeys.imageio.metadata.Directory;
|
||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
|
||||
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
|
||||
import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
|
||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
||||
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
|
||||
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
|
||||
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
|
||||
@ -62,7 +58,6 @@ import java.awt.color.ICC_ColorSpace;
|
||||
import java.awt.color.ICC_Profile;
|
||||
import java.awt.image.*;
|
||||
import java.io.*;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
@ -667,7 +662,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
||||
private void initDelegate(boolean seekForwardOnly, boolean ignoreMetadata) throws IOException {
|
||||
// JPEGSegmentImageInputStream that filters out/skips bad/unnecessary segments
|
||||
delegate.setInput(imageInput != null
|
||||
? new JPEGSegmentImageInputStream(new SubImageInputStream(imageInput, Long.MAX_VALUE), new JPEGSegmentStreamWarningDelegate())
|
||||
? new JPEGSegmentImageInputStream(new SubImageInputStream(imageInput, Long.MAX_VALUE), new JPEGSegmentWarningDelegate())
|
||||
: null, seekForwardOnly, ignoreMetadata);
|
||||
}
|
||||
|
||||
@ -705,6 +700,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
||||
}
|
||||
|
||||
private void initHeader(final int imageIndex) throws IOException {
|
||||
assertInput();
|
||||
if (imageIndex < 0) {
|
||||
throw new IndexOutOfBoundsException("imageIndex < 0: " + imageIndex);
|
||||
}
|
||||
@ -889,25 +885,25 @@ public final class JPEGImageReader extends ImageReaderBase {
|
||||
return jfxx.isEmpty() ? null : (JFXX) jfxx.get(0);
|
||||
}
|
||||
|
||||
private CompoundDirectory getExif() throws IOException {
|
||||
List<Application> exifSegments = getAppSegments(JPEG.APP1, "Exif");
|
||||
private EXIF getExif() throws IOException {
|
||||
List<Application> exif = getAppSegments(JPEG.APP1, "Exif");
|
||||
return exif.isEmpty() ? null : (EXIF) exif.get(0); // TODO: Can there actually be more Exif segments?
|
||||
}
|
||||
|
||||
if (!exifSegments.isEmpty()) {
|
||||
Application exif = exifSegments.get(0);
|
||||
int offset = exif.identifier.length() + 2; // Incl. pad
|
||||
|
||||
if (exif.data.length <= offset) {
|
||||
processWarningOccurred("Exif chunk has no data.");
|
||||
}
|
||||
else {
|
||||
// TODO: Consider returning ByteArrayImageInputStream from Segment.data()
|
||||
try (ImageInputStream stream = new ByteArrayImageInputStream(exif.data, offset, exif.data.length - offset)) {
|
||||
private CompoundDirectory parseExif(final EXIF exif) throws IOException {
|
||||
if (exif != null) {
|
||||
// Identifier is "Exif\0" + 1 byte pad
|
||||
if (exif.data.length > exif.identifier.length() + 2) {
|
||||
try (ImageInputStream stream = exif.exifData()) {
|
||||
return (CompoundDirectory) new TIFFReader().read(stream);
|
||||
}
|
||||
catch (IIOException e) {
|
||||
processWarningOccurred("Exif chunk is present, but can't be read: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
else {
|
||||
processWarningOccurred("Exif chunk has no data.");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -916,7 +912,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
||||
// TODO: Util method?
|
||||
static byte[] readFully(DataInput stream, int len) throws IOException {
|
||||
if (len == 0) {
|
||||
return null;
|
||||
throw new IllegalArgumentException("len == 0");
|
||||
}
|
||||
|
||||
byte[] data = new byte[len];
|
||||
@ -1089,109 +1085,26 @@ public final class JPEGImageReader extends ImageReaderBase {
|
||||
|
||||
if (thumbnails == null) {
|
||||
thumbnails = new ArrayList<>();
|
||||
ThumbnailReadProgressListener thumbnailProgressDelegator = new ThumbnailProgressDelegate();
|
||||
|
||||
JPEGSegmentWarningDelegate listenerDelegate = new JPEGSegmentWarningDelegate();
|
||||
|
||||
// Read JFIF thumbnails if present
|
||||
JFIF jfif = getJFIF();
|
||||
if (jfif != null && jfif.thumbnail != null) {
|
||||
// TODO: Check if the JFIF segment really has room for this thumbnail?
|
||||
thumbnails.add(new JFIFThumbnailReader(thumbnailProgressDelegator, imageIndex, thumbnails.size(), jfif));
|
||||
ThumbnailReader thumbnailReader = JFIFThumbnail.from(getJFIF(), listenerDelegate);
|
||||
if (thumbnailReader != null) {
|
||||
thumbnails.add(thumbnailReader);
|
||||
}
|
||||
|
||||
// Read JFXX thumbnails if present
|
||||
JFXX jfxx = getJFXX();
|
||||
if (jfxx != null && jfxx.thumbnail != null) {
|
||||
switch (jfxx.extensionCode) {
|
||||
case JFXX.JPEG:
|
||||
case JFXX.INDEXED:
|
||||
case JFXX.RGB:
|
||||
// TODO: Check if the JFXX segment really has room for this thumbnail?
|
||||
thumbnails.add(new JFXXThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), imageIndex, thumbnails.size(), jfxx));
|
||||
break;
|
||||
default:
|
||||
processWarningOccurred("Unknown JFXX extension code: " + jfxx.extensionCode);
|
||||
}
|
||||
thumbnailReader = JFXXThumbnail.from(getJFXX(), getThumbnailReader(), listenerDelegate);
|
||||
if (thumbnailReader != null) {
|
||||
thumbnails.add(thumbnailReader);
|
||||
}
|
||||
|
||||
// Read Exif thumbnails if present
|
||||
List<Application> exifSegments = getAppSegments(JPEG.APP1, "Exif");
|
||||
if (!exifSegments.isEmpty()) {
|
||||
Application exif = exifSegments.get(0);
|
||||
|
||||
// Identifier is "Exif\0" + 1 byte pad
|
||||
int dataOffset = exif.identifier.length() + 2;
|
||||
|
||||
if (exif.data.length <= dataOffset) {
|
||||
processWarningOccurred("Exif chunk has no data.");
|
||||
}
|
||||
else {
|
||||
ImageInputStream stream = new ByteArrayImageInputStream(exif.data, dataOffset, exif.data.length - dataOffset);
|
||||
try {
|
||||
CompoundDirectory exifMetadata = (CompoundDirectory) new TIFFReader().read(stream);
|
||||
|
||||
if (exifMetadata.directoryCount() == 2) {
|
||||
Directory ifd1 = exifMetadata.getDirectory(1);
|
||||
|
||||
// Compression: 1 = no compression, 6 = JPEG compression (default)
|
||||
Entry compressionEntry = ifd1.getEntryById(TIFF.TAG_COMPRESSION);
|
||||
int compression = compressionEntry == null ? 6 : ((Number) compressionEntry.getValue()).intValue();
|
||||
|
||||
if (compression == 6) {
|
||||
Entry jpegOffEntry = ifd1.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT);
|
||||
if (jpegOffEntry != null) {
|
||||
Entry jpegLenEntry = ifd1.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
|
||||
|
||||
// Test if Exif thumbnail is contained within the Exif segment (offset + length <= segment.length)
|
||||
long jpegOffset = ((Number) jpegOffEntry.getValue()).longValue();
|
||||
long jpegLength = jpegLenEntry != null ? ((Number) jpegLenEntry.getValue()).longValue() : -1;
|
||||
if (jpegLength > 0 && jpegOffset + jpegLength <= stream.length()) {
|
||||
// Verify first bytes are FFD8
|
||||
stream.seek(jpegOffset);
|
||||
stream.setByteOrder(ByteOrder.BIG_ENDIAN);
|
||||
if (stream.readUnsignedShort() == JPEG.SOI) {
|
||||
thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), 0, thumbnails.size(), ifd1, stream));
|
||||
}
|
||||
// TODO: Simplify this warning fallback stuff...
|
||||
else {
|
||||
processWarningOccurred("EXIF IFD with empty or incomplete JPEG thumbnail");
|
||||
}
|
||||
}
|
||||
else {
|
||||
processWarningOccurred("EXIF IFD with empty or incomplete JPEG thumbnail");
|
||||
}
|
||||
}
|
||||
else {
|
||||
processWarningOccurred("EXIF IFD with JPEG thumbnail missing JPEGInterchangeFormat tag");
|
||||
}
|
||||
}
|
||||
else if (compression == 1) {
|
||||
Entry stripOffEntry = ifd1.getEntryById(TIFF.TAG_STRIP_OFFSETS);
|
||||
if (stripOffEntry != null) {
|
||||
long stripOffset = ((Number) stripOffEntry.getValue()).longValue();
|
||||
|
||||
if (stripOffset < stream.length()) {
|
||||
// TODO: Verify length of Exif thumbnail vs length of segment like in JPEG
|
||||
// ...but this requires so many extra values... Instead move this logic to the
|
||||
// EXIFThumbnailReader?
|
||||
thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, getThumbnailReader(), 0, thumbnails.size(), ifd1, stream));
|
||||
}
|
||||
else {
|
||||
processWarningOccurred("EXIF IFD with empty or incomplete uncompressed thumbnail");
|
||||
}
|
||||
}
|
||||
else {
|
||||
processWarningOccurred("EXIF IFD with uncompressed thumbnail missing StripOffsets tag");
|
||||
}
|
||||
}
|
||||
else {
|
||||
processWarningOccurred("EXIF IFD with unknown compression (expected 1 or 6): " + compression);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IIOException e) {
|
||||
processWarningOccurred("Exif chunk present, but can't be read: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
EXIF exif = getExif();
|
||||
thumbnailReader = EXIFThumbnail.from(exif, parseExif(exif), getThumbnailReader(), listenerDelegate);
|
||||
if (thumbnailReader != null) {
|
||||
thumbnails.add(thumbnailReader);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1234,13 +1147,13 @@ public final class JPEGImageReader extends ImageReaderBase {
|
||||
public BufferedImage readThumbnail(int imageIndex, int thumbnailIndex) throws IOException {
|
||||
checkThumbnailBounds(imageIndex, thumbnailIndex);
|
||||
|
||||
// processThumbnailStarted(imageIndex, thumbnailIndex);
|
||||
// processThumbnailProgress(0f);
|
||||
processThumbnailStarted(imageIndex, thumbnailIndex);
|
||||
processThumbnailProgress(0f);
|
||||
|
||||
BufferedImage thumbnail = thumbnails.get(thumbnailIndex).read();;
|
||||
|
||||
// processThumbnailProgress(100f);
|
||||
// processThumbnailComplete();
|
||||
processThumbnailProgress(100f);
|
||||
processThumbnailComplete();
|
||||
|
||||
return thumbnail;
|
||||
}
|
||||
@ -1251,7 +1164,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
||||
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
|
||||
initHeader(imageIndex);
|
||||
|
||||
return new JPEGImage10Metadata(segments, getSOF(), getJFIF(), getJFXX(), getEmbeddedICCProfile(true), getAdobeDCT(), getExif());
|
||||
return new JPEGImage10Metadata(segments, getSOF(), getJFIF(), getJFXX(), getEmbeddedICCProfile(true), getAdobeDCT(), parseExif(getExif()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1376,24 +1289,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
private class ThumbnailProgressDelegate implements ThumbnailReadProgressListener {
|
||||
@Override
|
||||
public void thumbnailStarted(int imageIndex, int thumbnailIndex) {
|
||||
processThumbnailStarted(imageIndex, thumbnailIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void thumbnailProgress(float percentageDone) {
|
||||
processThumbnailProgress(percentageDone);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void thumbnailComplete() {
|
||||
processThumbnailComplete();
|
||||
}
|
||||
}
|
||||
|
||||
private class JPEGSegmentStreamWarningDelegate implements JPEGSegmentStreamWarningListener {
|
||||
private class JPEGSegmentWarningDelegate implements JPEGSegmentWarningListener {
|
||||
@Override
|
||||
public void warningOccurred(String warning) {
|
||||
processWarningOccurred(warning);
|
||||
|
@ -59,7 +59,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
|
||||
// TODO: Support multiple JPEG streams (SOI...EOI, SOI...EOI, ...) in a single file
|
||||
|
||||
private final ImageInputStream stream;
|
||||
private final JPEGSegmentStreamWarningListener warningListener;
|
||||
private final JPEGSegmentWarningListener warningListener;
|
||||
|
||||
private final ComponentIdSet componentIds = new ComponentIdSet();
|
||||
|
||||
@ -68,13 +68,13 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
|
||||
private Segment segment;
|
||||
|
||||
|
||||
JPEGSegmentImageInputStream(final ImageInputStream stream, final JPEGSegmentStreamWarningListener warningListener) {
|
||||
JPEGSegmentImageInputStream(final ImageInputStream stream, final JPEGSegmentWarningListener warningListener) {
|
||||
this.stream = notNull(stream, "stream");
|
||||
this.warningListener = notNull(warningListener, "warningListener");
|
||||
}
|
||||
|
||||
JPEGSegmentImageInputStream(final ImageInputStream stream) {
|
||||
this(stream, JPEGSegmentStreamWarningListener.NULL_LISTENER);
|
||||
this(stream, JPEGSegmentWarningListener.NULL_LISTENER);
|
||||
}
|
||||
|
||||
private void processWarningOccured(final String warning) {
|
||||
|
@ -33,10 +33,10 @@ package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
/**
|
||||
* JPEGSegmentStreamWarningListener
|
||||
*/
|
||||
interface JPEGSegmentStreamWarningListener {
|
||||
interface JPEGSegmentWarningListener {
|
||||
void warningOccurred(String warning);
|
||||
|
||||
JPEGSegmentStreamWarningListener NULL_LISTENER = new JPEGSegmentStreamWarningListener() {
|
||||
JPEGSegmentWarningListener NULL_LISTENER = new JPEGSegmentWarningListener() {
|
||||
@Override
|
||||
public void warningOccurred(final String warning) {}
|
||||
};
|
@ -31,12 +31,16 @@
|
||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.color.ColorSpace;
|
||||
import java.awt.image.*;
|
||||
import java.io.IOException;
|
||||
|
||||
import static com.twelvemonkeys.lang.Validate.isTrue;
|
||||
import static com.twelvemonkeys.lang.Validate.notNull;
|
||||
|
||||
/**
|
||||
* ThumbnailReader
|
||||
*
|
||||
@ -46,68 +50,156 @@ import java.io.IOException;
|
||||
*/
|
||||
abstract class ThumbnailReader {
|
||||
|
||||
private final ThumbnailReadProgressListener progressListener;
|
||||
protected final int imageIndex;
|
||||
protected final int thumbnailIndex;
|
||||
|
||||
protected ThumbnailReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex) {
|
||||
this.progressListener = progressListener != null ? progressListener : new NullProgressListener();
|
||||
this.imageIndex = imageIndex;
|
||||
this.thumbnailIndex = thumbnailIndex;
|
||||
}
|
||||
|
||||
protected final void processThumbnailStarted() {
|
||||
progressListener.thumbnailStarted(imageIndex, thumbnailIndex);
|
||||
}
|
||||
|
||||
protected final void processThumbnailProgress(float percentageDone) {
|
||||
progressListener.thumbnailProgress(percentageDone);
|
||||
}
|
||||
|
||||
protected final void processThumbnailComplete() {
|
||||
progressListener.thumbnailComplete();
|
||||
}
|
||||
|
||||
static protected BufferedImage readJPEGThumbnail(final ImageReader reader, final ImageInputStream stream) throws IOException {
|
||||
reader.setInput(stream);
|
||||
|
||||
return reader.read(0);
|
||||
}
|
||||
|
||||
static protected BufferedImage readRawThumbnail(final byte[] thumbnail, final int size, final int offset, int w, int h) {
|
||||
DataBufferByte buffer = new DataBufferByte(thumbnail, size, offset);
|
||||
WritableRaster raster;
|
||||
ColorModel cm;
|
||||
|
||||
if (thumbnail.length == w * h) {
|
||||
raster = Raster.createInterleavedRaster(buffer, w, h, w, 1, new int[] {0}, null);
|
||||
cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
|
||||
}
|
||||
else {
|
||||
raster = Raster.createInterleavedRaster(buffer, w, h, w * 3, 3, new int[] {0, 1, 2}, null);
|
||||
cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
|
||||
}
|
||||
|
||||
return new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
|
||||
}
|
||||
|
||||
public abstract BufferedImage read() throws IOException;
|
||||
|
||||
public abstract int getWidth() throws IOException;
|
||||
|
||||
public abstract int getHeight() throws IOException;
|
||||
|
||||
private static class NullProgressListener implements ThumbnailReadProgressListener {
|
||||
@Override
|
||||
public void thumbnailStarted(int imageIndex, int thumbnailIndex) {
|
||||
public IIOMetadata readMetadata() throws IOException {
|
||||
return null;
|
||||
}
|
||||
|
||||
static class UncompressedThumbnailReader extends ThumbnailReader {
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final byte[] data;
|
||||
private final int offset;
|
||||
|
||||
public UncompressedThumbnailReader(int width, int height, byte[] data) {
|
||||
this(width, height, data, 0);
|
||||
}
|
||||
|
||||
public UncompressedThumbnailReader(int width, int height, byte[] data, int offset) {
|
||||
this.width = isTrue(width > 0, width, "width");
|
||||
this.height = isTrue(height > 0, height, "height");;
|
||||
this.data = notNull(data, "data");
|
||||
this.offset = isTrue(offset >= 0 && offset < data.length, offset, "offset");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void thumbnailProgress(float percentageDone) {
|
||||
public BufferedImage read() throws IOException {
|
||||
DataBufferByte buffer = new DataBufferByte(data, data.length, offset);
|
||||
WritableRaster raster;
|
||||
ColorModel cm;
|
||||
|
||||
if (data.length == width * height) {
|
||||
raster = Raster.createInterleavedRaster(buffer, width, height, width, 1, new int[] {0}, null);
|
||||
cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
|
||||
}
|
||||
else {
|
||||
raster = Raster.createInterleavedRaster(buffer, width, height, width * 3, 3, new int[] {0, 1, 2}, null);
|
||||
cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
|
||||
}
|
||||
|
||||
return new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void thumbnailComplete() {
|
||||
public int getWidth() throws IOException {
|
||||
return width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() throws IOException {
|
||||
return height;
|
||||
}
|
||||
}
|
||||
|
||||
static class IndexedThumbnailReader extends ThumbnailReader {
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final byte[] palette;
|
||||
private final int paletteOff;
|
||||
private final byte[] data;
|
||||
private final int dataOff;
|
||||
|
||||
public IndexedThumbnailReader(final int width, int height, final byte[] palette, final int paletteOff, final byte[] data, final int dataOff) {
|
||||
this.width = isTrue(width > 0, width, "width");
|
||||
this.height = isTrue(height > 0, height, "height");;
|
||||
this.palette = notNull(palette, "palette");
|
||||
this.paletteOff = isTrue(paletteOff >= 0 && paletteOff < palette.length, paletteOff, "paletteOff");
|
||||
this.data = notNull(data, "data");
|
||||
this.dataOff = isTrue(dataOff >= 0 && dataOff < data.length, dataOff, "dataOff");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedImage read() throws IOException {
|
||||
// 256 RGB triplets
|
||||
int[] rgbs = new int[256];
|
||||
for (int i = 0; i < rgbs.length; i++) {
|
||||
rgbs[i] = (palette[paletteOff + 3 * i ] & 0xff) << 16
|
||||
| (palette[paletteOff + 3 * i + 1] & 0xff) << 8
|
||||
| (palette[paletteOff + 3 * i + 2] & 0xff);
|
||||
}
|
||||
|
||||
IndexColorModel icm = new IndexColorModel(8, rgbs.length, rgbs, 0, false, -1, DataBuffer.TYPE_BYTE);
|
||||
DataBufferByte buffer = new DataBufferByte(data, data.length - dataOff, dataOff);
|
||||
WritableRaster raster = Raster.createPackedRaster(buffer, width, height, 8, null);
|
||||
|
||||
return new BufferedImage(icm, raster, icm.isAlphaPremultiplied(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() throws IOException {
|
||||
return width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() throws IOException {
|
||||
return height;
|
||||
}
|
||||
}
|
||||
|
||||
static class JPEGThumbnailReader extends ThumbnailReader {
|
||||
private final ImageReader reader;
|
||||
private final ImageInputStream input;
|
||||
private final long offset;
|
||||
|
||||
private Dimension dimension;
|
||||
|
||||
public JPEGThumbnailReader(final ImageReader reader, final ImageInputStream input, final long offset) {
|
||||
this.reader = notNull(reader, "reader");
|
||||
this.input = notNull(input, "input");
|
||||
this.offset = isTrue(offset >= 0, offset, "offset");
|
||||
}
|
||||
|
||||
private void initReader() throws IOException {
|
||||
if (reader.getInput() != input) {
|
||||
input.seek(offset);
|
||||
reader.setInput(input);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedImage read() throws IOException {
|
||||
initReader();
|
||||
return reader.read(0, null);
|
||||
}
|
||||
|
||||
private Dimension readDimensions() throws IOException {
|
||||
if (dimension == null) {
|
||||
initReader();
|
||||
dimension = new Dimension(reader.getWidth(0), reader.getHeight(0));
|
||||
}
|
||||
|
||||
return dimension;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() throws IOException {
|
||||
return readDimensions().width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() throws IOException {
|
||||
return readDimensions().height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IIOMetadata readMetadata() throws IOException {
|
||||
initReader();
|
||||
return reader.getImageMetadata(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* AbstractThumbnailReaderTest
|
||||
@ -52,9 +53,9 @@ public abstract class AbstractThumbnailReaderTest {
|
||||
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
|
||||
}
|
||||
|
||||
protected abstract ThumbnailReader createReader(
|
||||
ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, ImageInputStream stream
|
||||
) throws IOException;
|
||||
protected final JPEGSegmentWarningListener listener = mock(JPEGSegmentWarningListener.class);
|
||||
|
||||
protected abstract ThumbnailReader createReader(ImageInputStream stream) throws IOException;
|
||||
|
||||
protected final ImageInputStream createStream(final String name) throws IOException {
|
||||
URL resource = getClass().getResource(name);
|
||||
|
@ -35,18 +35,19 @@ import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
|
||||
import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* EXIFThumbnailReaderTest
|
||||
@ -57,31 +58,28 @@ import static org.mockito.Mockito.*;
|
||||
*/
|
||||
public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||
|
||||
private final ImageReader thumbnailReader = ImageIO.getImageReadersByFormatName("jpeg").next();
|
||||
|
||||
@Override
|
||||
protected EXIFThumbnailReader createReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex, final ImageInputStream stream) throws IOException {
|
||||
protected ThumbnailReader createReader(final ImageInputStream stream) throws IOException {
|
||||
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP1, "Exif");
|
||||
stream.close();
|
||||
|
||||
assertNotNull(segments);
|
||||
assertFalse(segments.isEmpty());
|
||||
|
||||
TIFFReader reader = new TIFFReader();
|
||||
InputStream data = segments.get(0).data();
|
||||
if (data.read() < 0) {
|
||||
throw new AssertionError("EOF!");
|
||||
}
|
||||
JPEGSegment exifSegment = segments.get(0);
|
||||
InputStream data = exifSegment.segmentData();
|
||||
byte[] exifData = new byte[exifSegment.segmentLength() - 2];
|
||||
new DataInputStream(data).readFully(exifData);
|
||||
|
||||
ImageInputStream exifStream = ImageIO.createImageInputStream(data);
|
||||
CompoundDirectory ifds = (CompoundDirectory) reader.read(exifStream);
|
||||
|
||||
assertEquals(2, ifds.directoryCount());
|
||||
|
||||
return new EXIFThumbnailReader(progressListener, ImageIO.getImageReadersByFormatName("JPEG").next(), imageIndex, thumbnailIndex, ifds.getDirectory(1), exifStream);
|
||||
EXIF exif = new EXIF(exifData);
|
||||
return EXIFThumbnail.from(exif, (CompoundDirectory) new TIFFReader().read(exif.exifData()), thumbnailReader, listener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadJPEG() throws IOException {
|
||||
ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/cmyk-sample-multiple-chunk-icc.jpg"));
|
||||
ThumbnailReader reader = createReader(createStream("/jpeg/cmyk-sample-multiple-chunk-icc.jpg"));
|
||||
|
||||
assertEquals(114, reader.getWidth());
|
||||
assertEquals(160, reader.getHeight());
|
||||
@ -94,7 +92,7 @@ public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||
|
||||
@Test
|
||||
public void testReadRaw() throws IOException {
|
||||
ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/exif-rgb-thumbnail-sony-d700.jpg"));
|
||||
ThumbnailReader reader = createReader(createStream("/jpeg/exif-rgb-thumbnail-sony-d700.jpg"));
|
||||
|
||||
assertEquals(80, reader.getWidth());
|
||||
assertEquals(60, reader.getHeight());
|
||||
@ -104,28 +102,4 @@ public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||
assertEquals(80, thumbnail.getWidth());
|
||||
assertEquals(60, thumbnail.getHeight());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProgressListenerJPEG() throws IOException {
|
||||
ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class);
|
||||
|
||||
createReader(listener, 42, 43, createStream("/jpeg/cmyk-sample-multiple-chunk-icc.jpg")).read();
|
||||
|
||||
InOrder order = inOrder(listener);
|
||||
order.verify(listener).thumbnailStarted(42, 43);
|
||||
order.verify(listener, atLeastOnce()).thumbnailProgress(100f);
|
||||
order.verify(listener).thumbnailComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProgressListenerRaw() throws IOException {
|
||||
ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class);
|
||||
|
||||
createReader(listener, 0, 99, createStream("/jpeg/exif-rgb-thumbnail-sony-d700.jpg")).read();
|
||||
|
||||
InOrder order = inOrder(listener);
|
||||
order.verify(listener).thumbnailStarted(0, 99);
|
||||
order.verify(listener, atLeastOnce()).thumbnailProgress(100f);
|
||||
order.verify(listener).thumbnailComplete();
|
||||
}
|
||||
}
|
||||
|
@ -33,8 +33,8 @@ package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
@ -53,8 +53,9 @@ import static org.mockito.Mockito.*;
|
||||
* @version $Id: JFIFThumbnailReaderTest.java,v 1.0 04.05.12 15:56 haraldk Exp$
|
||||
*/
|
||||
public class JFIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||
|
||||
@Override
|
||||
protected JFIFThumbnailReader createReader(ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, ImageInputStream stream) throws IOException {
|
||||
protected ThumbnailReader createReader(ImageInputStream stream) throws IOException {
|
||||
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP0, "JFIF");
|
||||
stream.close();
|
||||
|
||||
@ -62,12 +63,54 @@ public class JFIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||
assertFalse(segments.isEmpty());
|
||||
|
||||
JPEGSegment segment = segments.get(0);
|
||||
return new JFIFThumbnailReader(progressListener, imageIndex, thumbnailIndex, JFIF.read(new DataInputStream(segment.segmentData()), segment.segmentLength()));
|
||||
|
||||
return JFIFThumbnail.from(JFIF.read(new DataInputStream(segment.segmentData()), segment.segmentLength()), listener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromNull() {
|
||||
assertNull(JFIFThumbnail.from(null, listener));
|
||||
|
||||
verify(listener, never()).warningOccurred(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromNullThumbnail() {
|
||||
assertNull(JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 0, 0, null), listener));
|
||||
|
||||
verify(listener, never()).warningOccurred(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromEmpty() {
|
||||
assertNull(JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 0, 0, new byte[0]), listener));
|
||||
|
||||
verify(listener, never()).warningOccurred(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromTruncated() {
|
||||
assertNull(JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 255, 170, new byte[99]), listener));
|
||||
|
||||
verify(listener, only()).warningOccurred(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromValid() throws IOException {
|
||||
ThumbnailReader reader = JFIFThumbnail.from(new JFIF(1, 1, 0, 1, 1, 30, 20, new byte[30 * 20 * 3]), listener);
|
||||
assertNotNull(reader);
|
||||
|
||||
verify(listener, never()).warningOccurred(anyString());
|
||||
|
||||
// Sanity check below
|
||||
assertEquals(30, reader.getWidth());
|
||||
assertEquals(20, reader.getHeight());
|
||||
assertNotNull(reader.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadRaw() throws IOException {
|
||||
ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/jfif-jfif-and-exif-thumbnail-sharpshot-iphone.jpg"));
|
||||
ThumbnailReader reader = createReader(createStream("/jpeg/jfif-jfif-and-exif-thumbnail-sharpshot-iphone.jpg"));
|
||||
|
||||
assertEquals(131, reader.getWidth());
|
||||
assertEquals(122, reader.getHeight());
|
||||
@ -80,7 +123,7 @@ public class JFIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||
|
||||
@Test
|
||||
public void testReadNonSpecGray() throws IOException {
|
||||
ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/jfif-grayscale-thumbnail.jpg"));
|
||||
ThumbnailReader reader = createReader(createStream("/jpeg/jfif-grayscale-thumbnail.jpg"));
|
||||
|
||||
assertEquals(127, reader.getWidth());
|
||||
assertEquals(76, reader.getHeight());
|
||||
@ -91,16 +134,4 @@ public class JFIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||
assertEquals(127, thumbnail.getWidth());
|
||||
assertEquals(76, thumbnail.getHeight());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProgressListenerRaw() throws IOException {
|
||||
ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class);
|
||||
|
||||
createReader(listener, 0, 99, createStream("/jpeg/jfif-jfif-and-exif-thumbnail-sharpshot-iphone.jpg")).read();
|
||||
|
||||
InOrder order = inOrder(listener);
|
||||
order.verify(listener).thumbnailStarted(0, 99);
|
||||
order.verify(listener, atLeastOnce()).thumbnailProgress(100f);
|
||||
order.verify(listener).thumbnailComplete();
|
||||
}
|
||||
}
|
||||
|
@ -33,10 +33,12 @@ package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.DataInputStream;
|
||||
@ -44,6 +46,7 @@ import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
@ -54,8 +57,10 @@ import static org.mockito.Mockito.*;
|
||||
* @version $Id: JFXXThumbnailReaderTest.java,v 1.0 04.05.12 15:56 haraldk Exp$
|
||||
*/
|
||||
public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||
private final ImageReader thumbnailReader = ImageIO.getImageReadersByFormatName("jpeg").next();
|
||||
|
||||
@Override
|
||||
protected JFXXThumbnailReader createReader(ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, ImageInputStream stream) throws IOException {
|
||||
protected ThumbnailReader createReader(ImageInputStream stream) throws IOException {
|
||||
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP0, "JFXX");
|
||||
stream.close();
|
||||
|
||||
@ -63,12 +68,81 @@ public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||
assertFalse(segments.isEmpty());
|
||||
|
||||
JPEGSegment jfxx = segments.get(0);
|
||||
return new JFXXThumbnailReader(progressListener, ImageIO.getImageReadersByFormatName("jpeg").next(), imageIndex, thumbnailIndex, JFXX.read(new DataInputStream(jfxx.segmentData()), jfxx.length()));
|
||||
return JFXXThumbnail.from(JFXX.read(new DataInputStream(jfxx.segmentData()), jfxx.length()), thumbnailReader, listener);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
thumbnailReader.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromNull() {
|
||||
assertNull(JFXXThumbnail.from(null, thumbnailReader, listener));
|
||||
|
||||
verify(listener, never()).warningOccurred(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromNullThumbnail() {
|
||||
assertNull(JFXXThumbnail.from(new JFXX(JFXX.JPEG, null), thumbnailReader, listener));
|
||||
|
||||
verify(listener, only()).warningOccurred(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromEmpty() {
|
||||
assertNull(JFXXThumbnail.from(new JFXX(JFXX.JPEG, new byte[0]), thumbnailReader, listener));
|
||||
|
||||
verify(listener, only()).warningOccurred(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromTruncatedJPEG() {
|
||||
assertNull(JFXXThumbnail.from(new JFXX(JFXX.JPEG, new byte[99]), thumbnailReader, listener));
|
||||
|
||||
verify(listener, only()).warningOccurred(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromTruncatedRGB() {
|
||||
byte[] thumbnail = new byte[765];
|
||||
thumbnail[0] = (byte) 160;
|
||||
thumbnail[1] = 90;
|
||||
assertNull(JFXXThumbnail.from(new JFXX(JFXX.RGB, thumbnail), thumbnailReader, listener));
|
||||
|
||||
verify(listener, only()).warningOccurred(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromTruncatedIndexed() {
|
||||
byte[] thumbnail = new byte[365];
|
||||
thumbnail[0] = (byte) 160;
|
||||
thumbnail[1] = 90;
|
||||
assertNull(JFXXThumbnail.from(new JFXX(JFXX.INDEXED, thumbnail), thumbnailReader, listener));
|
||||
|
||||
verify(listener, only()).warningOccurred(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromValid() throws IOException {
|
||||
byte[] thumbnail = new byte[14];
|
||||
thumbnail[0] = 2;
|
||||
thumbnail[1] = 2;
|
||||
ThumbnailReader reader = JFXXThumbnail.from(new JFXX(JFXX.RGB, thumbnail), thumbnailReader, listener);
|
||||
assertNotNull(reader);
|
||||
|
||||
verify(listener, never()).warningOccurred(anyString());
|
||||
|
||||
// Sanity check below
|
||||
assertEquals(2, reader.getWidth());
|
||||
assertEquals(2, reader.getHeight());
|
||||
assertNotNull(reader.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadJPEG() throws IOException {
|
||||
ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg"));
|
||||
ThumbnailReader reader = createReader(createStream("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg"));
|
||||
|
||||
assertEquals(80, reader.getWidth());
|
||||
assertEquals(60, reader.getHeight());
|
||||
@ -81,16 +155,4 @@ public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest {
|
||||
|
||||
// TODO: Test JFXX indexed thumbnail
|
||||
// TODO: Test JFXX RGB thumbnail
|
||||
|
||||
@Test
|
||||
public void testProgressListenerRaw() throws IOException {
|
||||
ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class);
|
||||
|
||||
createReader(listener, 0, 99, createStream("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg")).read();
|
||||
|
||||
InOrder order = inOrder(listener);
|
||||
order.verify(listener).thumbnailStarted(0, 99);
|
||||
order.verify(listener, atLeastOnce()).thumbnailProgress(100f);
|
||||
order.verify(listener).thumbnailComplete();
|
||||
}
|
||||
}
|
||||
|
@ -441,6 +441,7 @@ final class TGAImageReader extends ImageReaderBase {
|
||||
WritableRaster rowRaster = rawType.createBufferedImage(width, 1).getRaster();
|
||||
|
||||
processThumbnailStarted(imageIndex, thumbnailIndex);
|
||||
processThumbnailProgress(0f);
|
||||
|
||||
// Thumbnail is always stored non-compressed, no need for RLE support
|
||||
imageInput.seek(extensions.getThumbnailOffset() + 2);
|
||||
@ -468,6 +469,7 @@ final class TGAImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
processThumbnailProgress(100f);
|
||||
processThumbnailComplete();
|
||||
|
||||
return destination;
|
||||
|
Loading…
x
Reference in New Issue
Block a user