Implemented all-new JPEGSegmentIIS that filters out bad JPEG segments before passing on to the native reader.

Implemented JFIF, JFXX and EXIF thumbnail reading.
Added loads of test cases for special cases found in the wild.
This commit is contained in:
Harald Kuhr 2012-02-01 16:01:34 +01:00
parent 1830808d56
commit f2e3f7ed03
13 changed files with 1010 additions and 127 deletions

View File

@ -29,13 +29,18 @@
package com.twelvemonkeys.imageio.plugins.jpeg; package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.image.ImageUtil; import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.image.InverseColorMapIndexColorModel;
import com.twelvemonkeys.imageio.ImageReaderBase; import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.color.ColorSpaces; import com.twelvemonkeys.imageio.color.ColorSpaces;
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
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.util.IIOUtil;
import com.twelvemonkeys.imageio.util.ProgressListenerBase; import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.lang.Validate; import com.twelvemonkeys.lang.Validate;
@ -64,7 +69,8 @@ import java.util.List;
* @version $Id: JPEGImageReader.java,v 1.0 24.01.11 16.37 haraldk Exp$ * @version $Id: JPEGImageReader.java,v 1.0 24.01.11 16.37 haraldk Exp$
*/ */
public class JPEGImageReader extends ImageReaderBase { public class JPEGImageReader extends ImageReaderBase {
// TODO: Fix the (stream) metadata inconsistency issues // TODO: Fix the (stream) metadata inconsistency issues.
// - Sun JPEGMetadata class does not (and can not be made to) support CMYK data.. We need to create all new metadata classes
private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug")); private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug"));
@ -107,11 +113,14 @@ public class JPEGImageReader extends ImageReaderBase {
/** Our JPEG reading delegate */ /** Our JPEG reading delegate */
private final ImageReader delegate; private final ImageReader delegate;
/** Listens to progress updates in the delegate, and delegates back to this instance */
private final ProgressDelegator progressDelegator; private final ProgressDelegator progressDelegator;
/** Cached JPEG app segments */ /** Cached JPEG app segments */
private List<JPEGSegment> segments; private List<JPEGSegment> segments;
private List<BufferedImage> thumbnails;
JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) { JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) {
super(provider); super(provider);
this.delegate = Validate.notNull(delegate); this.delegate = Validate.notNull(delegate);
@ -125,12 +134,11 @@ public class JPEGImageReader extends ImageReaderBase {
delegate.addIIOReadWarningListener(progressDelegator); delegate.addIIOReadWarningListener(progressDelegator);
} }
// TODO: Delegate all methods?!
@Override @Override
protected void resetMembers() { protected void resetMembers() {
delegate.reset(); delegate.reset();
segments = null; segments = null;
thumbnails = null;
installListeners(); installListeners();
} }
@ -182,7 +190,6 @@ public class JPEGImageReader extends ImageReaderBase {
// + original color profile should be an option // + original color profile should be an option
).iterator(); ).iterator();
} }
return types; return types;
@ -198,7 +205,8 @@ public class JPEGImageReader extends ImageReaderBase {
public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) {
super.setInput(input, seekForwardOnly, ignoreMetadata); super.setInput(input, seekForwardOnly, ignoreMetadata);
delegate.setInput(input, seekForwardOnly, ignoreMetadata); // JPEGSegmentImageInputStream that filters out/skips bad/unnecessary segments
delegate.setInput(imageInput != null ? new JPEGSegmentImageInputStream(imageInput) : null, seekForwardOnly, ignoreMetadata);
} }
@Override @Override
@ -283,7 +291,7 @@ public class JPEGImageReader extends ImageReaderBase {
--------------------------------------------------------------------------------------------------------------*/ --------------------------------------------------------------------------------------------------------------*/
// TODO: Fix this algorithm to behave like above, except the presence of JFIF APP0 might mean YCbCr, gray or CMYK. // TODO: Fix this algorithm to behave like above, except the presence of JFIF APP0 might mean YCbCr, gray *or CMYK*.
// AdobeApp14 with transform either 1 or 2 can be trusted to be YCC/YCCK respectively, transform 0 means 1 component gray, 3 comp rgb, 4 comp cmyk // AdobeApp14 with transform either 1 or 2 can be trusted to be YCC/YCCK respectively, transform 0 means 1 component gray, 3 comp rgb, 4 comp cmyk
SOF startOfFrame = getSOF(); SOF startOfFrame = getSOF();
@ -312,6 +320,15 @@ public class JPEGImageReader extends ImageReaderBase {
// TODO: Move to getImageTypes + add native color space if profile != null // TODO: Move to getImageTypes + add native color space if profile != null
).iterator(); ).iterator();
} }
else if (!imageTypes.hasNext() && profile != null) {
// TODO: Bad ICC profiles need these substitute types here, but it will still crash in readRaster
srcCs = null;
imageTypes = Arrays.asList(
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR),
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB),
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR)
).iterator();
}
// ...else blow up as there's no possible types to decode into... // ...else blow up as there's no possible types to decode into...
BufferedImage image = getDestination(param, imageTypes, origWidth, origHeight); BufferedImage image = getDestination(param, imageTypes, origWidth, origHeight);
@ -331,8 +348,28 @@ public class JPEGImageReader extends ImageReaderBase {
convert = new ColorConvertOp(srcCs, image.getColorModel().getColorSpace(), null); convert = new ColorConvertOp(srcCs, image.getColorModel().getColorSpace(), null);
} }
else if (replacement != null) { else if (replacement != null) {
// Handle inconsistencies
if (startOfFrame.componentsInFrame != replacement.getNumComponents()) {
if (startOfFrame.componentsInFrame < 4 && transform == AdobeDCT.YCCK) {
processWarningOccurred(String.format(
"Invalid Adobe App14 marker. Indicates YCCK/CMYK data, but SOFn has %d color components. " +
"Ignoring Adobe App14 marker, assuming YCC/RGB data.",
startOfFrame.componentsInFrame
));
transform = AdobeDCT.YCC;
}
// If ICC profile number of components and startOfFrame does not match, ignore ICC profile
processWarningOccurred(String.format(
"Embedded ICC color profile is incompatible with image data. " +
"Profile indicates %d components, but SOFn has %d color components. " +
"Ignoring ICC profile, assuming YCC/RGB data.",
replacement.getNumComponents(), startOfFrame.componentsInFrame
));
srcCs = null;
}
// NOTE: Avoid using CCOp if same color space, as it's more compatible that way // NOTE: Avoid using CCOp if same color space, as it's more compatible that way
if (replacement != image.getColorModel().getColorSpace()) { else if (replacement != image.getColorModel().getColorSpace()) {
// TODO: Use profiles instead of CS, if ICC profiles? Avoid creating expensive CS. // TODO: Use profiles instead of CS, if ICC profiles? Avoid creating expensive CS.
convert = new ColorConvertOp(replacement, image.getColorModel().getColorSpace(), null); convert = new ColorConvertOp(replacement, image.getColorModel().getColorSpace(), null);
} }
@ -352,7 +389,7 @@ public class JPEGImageReader extends ImageReaderBase {
// convert = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_sRGB), image.getColorModel().getColorSpace(), null); // convert = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_sRGB), image.getColorModel().getColorSpace(), null);
// } // }
else if (profile != null) { else if (profile != null) {
processWarningOccurred("Image contains an ICC color profile that is incompatible with Java 2D, color profile ignored."); processWarningOccurred("Embedded ICC color profile is incompatible with Java 2D, color profile will be ignored.");
} }
// We'll need a read param // We'll need a read param
@ -424,11 +461,11 @@ public class JPEGImageReader extends ImageReaderBase {
} }
} }
finally { finally {
// NOTE: Would be cleaner to clone the param, unfortunately it can't be done easily...
param.setSourceRegion(origSourceRegion);
// Restore normal read progress processing // Restore normal read progress processing
progressDelegator.resetProgressRange(); progressDelegator.resetProgressRange();
// NOTE: Would be cleaner to clone the param, unfortunately it can't be done easily...
param.setSourceRegion(origSourceRegion);
} }
processImageComplete(); processImageComplete();
@ -437,8 +474,8 @@ public class JPEGImageReader extends ImageReaderBase {
} }
private ICC_Profile ensureDisplayProfile(final ICC_Profile profile) { private ICC_Profile ensureDisplayProfile(final ICC_Profile profile) {
// TODO: This is probably not the right way to do it... :-P // NOTE: This is probably not the right way to do it... :-P
// TODO: Consider moving to ColorSpaces class or new class in imageio.color package // TODO: Consider moving method to ColorSpaces class or new class in imageio.color package
// NOTE: Workaround for the ColorConvertOp treating the input as relative colorimetric, // NOTE: Workaround for the ColorConvertOp treating the input as relative colorimetric,
// if the FIRST profile has class OUTPUT, regardless of the actual rendering intent in that profile... // if the FIRST profile has class OUTPUT, regardless of the actual rendering intent in that profile...
@ -494,7 +531,7 @@ public class JPEGImageReader extends ImageReaderBase {
} }
} }
private Directory getEXIFMetadata() throws IOException { private CompoundDirectory getEXIFMetadata() throws IOException {
List<JPEGSegment> exifSegments = getAppSegments(JPEG.APP1, "Exif"); List<JPEGSegment> exifSegments = getAppSegments(JPEG.APP1, "Exif");
if (!exifSegments.isEmpty()) { if (!exifSegments.isEmpty()) {
@ -505,16 +542,77 @@ public class JPEGImageReader extends ImageReaderBase {
ImageInputStream stream = ImageIO.createImageInputStream(data); ImageInputStream stream = ImageIO.createImageInputStream(data);
@SuppressWarnings("UnnecessaryLocalVariable") CompoundDirectory exifMetadata = (CompoundDirectory) new EXIFReader().read(stream);
Directory exifMetadata = new EXIFReader().read(stream); /**/
if (exifMetadata.directoryCount() == 2) {
Directory ifd1 = exifMetadata.getDirectory(1);
Entry compression = ifd1.getEntryById(TIFF.TAG_COMPRESSION);
if (compression != null && compression.getValue().equals(1)) {
// Read ImageWidth, ImageLength (height) and BitsPerSample (=8 8 8, always)
// PhotometricInterpretation (2=RGB, 6=YCbCr), SamplesPerPixel (=3, always),
Entry width = ifd1.getEntryById(TIFF.TAG_IMAGE_WIDTH);
Entry height = ifd1.getEntryById(TIFF.TAG_IMAGE_HEIGHT);
// Entry jpegOffset = exifMetadata.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT); if (width == null || height == null) {
// if (jpegOffset != null) { throw new IIOException("Missing dimensions for RAW EXIF thumbnail");
// stream.seek((Long) jpegOffset.getValue()); }
// BufferedImage image = ImageIO.read(IIOUtil.createStreamAdapter(stream));
// System.err.println("image: " + image); Entry bitsPerSample = ifd1.getEntryById(TIFF.TAG_BITS_PER_SAMPLE);
// showIt(image, "Thumbnail"); Entry samplesPerPixel = ifd1.getEntryById(TIFF.TAG_SAMPLES_PER_PIXELS);
// } Entry photometricInterpretation = ifd1.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 bits per sample for RAW EXIF thumbnail: " + bitsPerSample.getValueAsString());
}
}
if (samplesPerPixel != null && (Integer) samplesPerPixel.getValue() != 3) {
throw new IIOException("Unknown samples per pixel for RAW EXIF thumbnail: " + samplesPerPixel.getValueAsString());
}
int interpretation = photometricInterpretation != null ? ((Number) photometricInterpretation.getValue()).intValue() : 2;
// Read raw image data, either RGB or YCbCr
byte[] thumbData = readFully(stream, w * h * 3);
DataBuffer buffer = new DataBufferByte(thumbData, thumbData.length);
WritableRaster raster = Raster.createInterleavedRaster(buffer, w, h, w * 3, 3, new int[] {0, 1, 2}, null);
switch (interpretation) {
case 2:
// RGB
break;
case 6:
// YCbCr
YCbCrConverter.convertYCbCr2RGB(raster);
break;
default:
throw new IIOException("Unknown photometric interpretation for RAW EXIF thumbail: " + interpretation);
}
ColorModel cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB),false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
thumbnails.add(new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null));
}
else if (compression == null || compression.getValue().equals(6)) {
Entry jpegOffset = ifd1.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT);
if (jpegOffset != null) {
stream.seek((Long) jpegOffset.getValue());
InputStream adapter = IIOUtil.createStreamAdapter(stream);
BufferedImage exifThumb = ImageIO.read(adapter);
if (exifThumb != null) {
thumbnails.add(exifThumb);
}
adapter.close();
}
}
}
//*/
return exifMetadata; return exifMetadata;
} }
@ -540,10 +638,6 @@ public class JPEGImageReader extends ImageReaderBase {
return appSegments; return appSegments;
} }
public boolean isJFIFAPP0Present() throws IOException {
return !(getAppSegments(JPEG.APP0, "JFIF").isEmpty() && getAppSegments(JPEG.APP0, "JFXX").isEmpty());
}
private SOF getSOF() throws IOException { private SOF getSOF() throws IOException {
for (JPEGSegment segment : segments) { for (JPEGSegment segment : segments) {
if (JPEG.SOF0 <= segment.marker() && segment.marker() <= JPEG.SOF3 || if (JPEG.SOF0 <= segment.marker() && segment.marker() <= JPEG.SOF3 ||
@ -581,6 +675,7 @@ public class JPEGImageReader extends ImageReaderBase {
} }
private AdobeDCT getAdobeDCT() throws IOException { private AdobeDCT getAdobeDCT() throws IOException {
// TODO: Investigate http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6355567: 33/35 byte Adobe app14 markers
List<JPEGSegment> adobe = getAppSegments(JPEG.APP14, "Adobe"); List<JPEGSegment> adobe = getAppSegments(JPEG.APP14, "Adobe");
if (!adobe.isEmpty()) { if (!adobe.isEmpty()) {
@ -598,6 +693,58 @@ public class JPEGImageReader extends ImageReaderBase {
return null; return null;
} }
private JFIF getJFIF() throws IOException{
List<JPEGSegment> jfif = getAppSegments(JPEG.APP0, "JFIF");
if (!jfif.isEmpty()) {
JPEGSegment segment = jfif.get(0);
DataInputStream stream = new DataInputStream(segment.data());
int x, y;
return new JFIF(
stream.readUnsignedByte(),
stream.readUnsignedByte(),
stream.readUnsignedByte(),
stream.readUnsignedShort(),
stream.readUnsignedShort(),
x = stream.readUnsignedByte(),
y = stream.readUnsignedByte(),
readFully(stream, x * y)
);
}
return null;
}
private JFXX getJFXX() throws IOException {
List<JPEGSegment> jfxx = getAppSegments(JPEG.APP0, "JFXX");
if (!jfxx.isEmpty()) {
JPEGSegment segment = jfxx.get(0);
DataInputStream stream = new DataInputStream(segment.data());
return new JFXX(
stream.readUnsignedByte(),
readFully(stream, segment.length() - 1)
);
}
return null;
}
private byte[] readFully(DataInput stream, int len) throws IOException {
if (len == 0) {
return null;
}
byte[] data = new byte[len];
stream.readFully(data);
return data;
}
private ICC_Profile getEmbeddedICCProfile() throws IOException { private ICC_Profile getEmbeddedICCProfile() throws IOException {
// ICC v 1.42 (2006) annex B: // ICC v 1.42 (2006) annex B:
// APP2 marker (0xFFE2) + 2 byte length + ASCII 'ICC_PROFILE' + 0 (termination) // APP2 marker (0xFFE2) + 2 byte length + ASCII 'ICC_PROFILE' + 0 (termination)
@ -623,18 +770,33 @@ public class JPEGImageReader extends ImageReaderBase {
int chunkNumber = stream.readUnsignedByte(); int chunkNumber = stream.readUnsignedByte();
int chunkCount = stream.readUnsignedByte(); int chunkCount = stream.readUnsignedByte();
InputStream[] streams = new InputStream[chunkCount]; // Some weird JPEGs use 0-based indexes... count == 0 and all numbers == 0.
streams[chunkNumber - 1] = stream; // Others use count == 1, and all numbers == 1.
// Handle these by issuing warning
boolean badICC = false;
if (chunkNumber < 1) {
badICC = true;
processWarningOccurred("Unexpected ICC profile chunk index: " + chunkNumber + ". Ignoring indexes, assuming chunks are in sequence.");
}
if (chunkCount != segments.size()) {
badICC = true;
processWarningOccurred("Unexpected ICC profile chunk count: " + chunkCount + ". Ignoring count, assuming " + segments.size() + " chunks in sequence.");
}
for (int i = 1; i < chunkCount; i++) { int count = badICC ? segments.size() : chunkCount;
InputStream[] streams = new InputStream[count];
streams[badICC ? 0 : chunkNumber - 1] = stream;
for (int i = 1; i < count; i++) {
stream = new DataInputStream(segments.get(i).data()); stream = new DataInputStream(segments.get(i).data());
chunkNumber = stream.readUnsignedByte(); chunkNumber = stream.readUnsignedByte();
if (stream.readUnsignedByte() != chunkCount) {
if (!badICC && stream.readUnsignedByte() != chunkCount) {
throw new IIOException(String.format("Bad number of 'ICC_PROFILE' chunks.")); throw new IIOException(String.format("Bad number of 'ICC_PROFILE' chunks."));
} }
streams[chunkNumber - 1] = stream; streams[badICC ? i : chunkNumber - 1] = stream;
} }
return ICC_Profile.getInstance(new SequenceInputStream(Collections.enumeration(Arrays.asList(streams)))); return ICC_Profile.getInstance(new SequenceInputStream(Collections.enumeration(Arrays.asList(streams))));
@ -668,32 +830,121 @@ public class JPEGImageReader extends ImageReaderBase {
// TODO: Fix thumbnails based on JFIF and EXIF thumbnails // TODO: Fix thumbnails based on JFIF and EXIF thumbnails
@Override @Override
public boolean readerSupportsThumbnails() { public boolean readerSupportsThumbnails() {
return delegate.readerSupportsThumbnails(); return true; // We support EXIF thumbnails, even if no JFIF thumbnail is present
}
private void readThumbnailMetadata(int imageIndex) throws IOException {
checkBounds(imageIndex);
if (thumbnails == null) {
thumbnails = new ArrayList<BufferedImage>();
JFIF jfif = getJFIF();
if (jfif != null && jfif.thumbnail != null) {
// TODO: Actually decode jfif
thumbnails.add(new BufferedImage(jfif.xThumbnail, jfif.yThumbnail, BufferedImage.TYPE_3BYTE_BGR));
}
JFXX jfxx = getJFXX();
if (jfxx != null && jfxx.thumbnail != null) {
switch (jfxx.extensionCode) {
case JFXX.JPEG:
thumbnails.add(ImageIO.read(new ByteArrayInputStream(jfxx.thumbnail)));
break;
case JFXX.INDEXED:
// 1 byte: xThumb
// 1 byte: yThumb
// 768 bytes: palette
// x * y bytes: 8 bit indexed pixels
int w = jfxx.thumbnail[0] & 0xff;
int h = jfxx.thumbnail[1] & 0xff;
int[] rgbs = new int[256];
for (int i = 0; i < rgbs.length; i++) {
int rgb = (jfxx.thumbnail[3 * i] & 0xff) << 16|
(jfxx.thumbnail[3 * i] & 0xff) << 8 |
(jfxx.thumbnail[3 * i] & 0xff);
rgbs[i] = rgb;
}
IndexColorModel icm = new InverseColorMapIndexColorModel(8, rgbs.length, rgbs, 0, false, -1, DataBuffer.TYPE_BYTE);
DataBufferByte buffer = new DataBufferByte(jfxx.thumbnail, jfxx.thumbnail.length - 770, 770);
WritableRaster raster = Raster.createPackedRaster(buffer, w, h, 8, null);
thumbnails.add(new BufferedImage(icm, raster, icm.isAlphaPremultiplied(), null));
break;
case JFXX.RGB:
// 1 byte: xThumb
// 1 byte: yThumb
// 3 * x * y bytes: 24 bit RGB pixels
w = jfxx.thumbnail[0] & 0xff;
h = jfxx.thumbnail[1] & 0xff;
buffer = new DataBufferByte(jfxx.thumbnail, jfxx.thumbnail.length - 2, 2);
raster = Raster.createInterleavedRaster(buffer, w, h, w * 3, 3, new int[] {0, 1, 2}, null);
ColorModel cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB),false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
thumbnails.add(new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null));
break;
default:
processWarningOccurred("Unknown JFXX extension code: " + jfxx.extensionCode);
}
}
// TODO: Ideally we want to decode image data in getThumbnail, less ideally here, but at least not in getEXIFMetadata()
CompoundDirectory exifMetadata = getEXIFMetadata();
// System.err.println("exifMetadata: " + exifMetadata);
// if (exifMetadata != null && exifMetadata.directoryCount() >= 2) {
// Directory ifd1 = exifMetadata.getDirectory(1);
// if (ifd1.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT) != null) {
// }
// }
}
} }
@Override @Override
public boolean hasThumbnails(int imageIndex) throws IOException { public int getNumThumbnails(final int imageIndex) throws IOException {
return delegate.hasThumbnails(imageIndex); readThumbnailMetadata(imageIndex);
return thumbnails.size();
} }
@Override private void checkThumbnailBounds(int imageIndex, int thumbnailIndex) throws IOException {
public int getNumThumbnails(int imageIndex) throws IOException { Validate.isTrue(thumbnailIndex >= 0, thumbnailIndex, "thumbnailIndex < 0; %d");
return delegate.getNumThumbnails(imageIndex); Validate.isTrue(getNumThumbnails(imageIndex) > thumbnailIndex, thumbnailIndex, "thumbnailIndex >= numThumbnails; %d");
} }
@Override @Override
public int getThumbnailWidth(int imageIndex, int thumbnailIndex) throws IOException { public int getThumbnailWidth(int imageIndex, int thumbnailIndex) throws IOException {
return delegate.getThumbnailWidth(imageIndex, thumbnailIndex); checkThumbnailBounds(imageIndex, thumbnailIndex);
return thumbnails.get(thumbnailIndex).getWidth();
} }
@Override @Override
public int getThumbnailHeight(int imageIndex, int thumbnailIndex) throws IOException { public int getThumbnailHeight(int imageIndex, int thumbnailIndex) throws IOException {
return delegate.getThumbnailHeight(imageIndex, thumbnailIndex); checkThumbnailBounds(imageIndex, thumbnailIndex);
return thumbnails.get(thumbnailIndex).getHeight();
} }
@Override @Override
public BufferedImage readThumbnail(int imageIndex, int thumbnailIndex) throws IOException { public BufferedImage readThumbnail(int imageIndex, int thumbnailIndex) throws IOException {
return delegate.readThumbnail(imageIndex, thumbnailIndex); checkThumbnailBounds(imageIndex, thumbnailIndex);
// TODO: Thumbnail progress listeners...
BufferedImage thumbnail = thumbnails.get(thumbnailIndex);
processThumbnailStarted(imageIndex, thumbnailIndex);
// For now: Clone. TODO: Do the actual decoding/reading here.
thumbnail = new BufferedImage(thumbnail.getColorModel(), thumbnail.copyData(null), thumbnail.getColorModel().isAlphaPremultiplied(), null);
processThumbnailProgress(100f);
processThumbnailComplete();
return thumbnail;
} }
private static void invertCMYK(final Raster raster) { private static void invertCMYK(final Raster raster) {
@ -910,7 +1161,6 @@ public class JPEGImageReader extends ImageReaderBase {
public void warningOccurred(ImageReader source, String warning) { public void warningOccurred(ImageReader source, String warning) {
processWarningOccurred(warning); processWarningOccurred(warning);
} }
} }
private static class SOF { private static class SOF {
@ -980,6 +1230,87 @@ public class JPEGImageReader extends ImageReaderBase {
} }
} }
private static class JFIF {
private final int majorVersion;
private final int minorVersion;
private final int units;
private final int xDensity;
private final int yDensity;
private final int xThumbnail;
private final int yThumbnail;
private final byte[] thumbnail;
public JFIF(int majorVersion, int minorVersion, int units, int xDensity, int yDensity, int xThumbnail, int yThumbnail, byte[] thumbnail) {
this.majorVersion = majorVersion;
this.minorVersion = minorVersion;
this.units = units;
this.xDensity = xDensity;
this.yDensity = yDensity;
this.xThumbnail = xThumbnail;
this.yThumbnail = yThumbnail;
this.thumbnail = thumbnail;
}
@Override
public String toString() {
return String.format("JFIF v%d.%02d %dx%d %s (%s)", majorVersion, minorVersion, xDensity, yDensity, unitsAsString(), thumbnailToString());
}
private String unitsAsString() {
switch (units) {
case 0:
return "(aspect only)";
case 1:
return "dpi";
case 2:
return "dpcm";
default:
return "(unknown unit)";
}
}
private String thumbnailToString() {
if (xThumbnail == 0 || yThumbnail == 0) {
return "no thumbnail";
}
return String.format("thumbnail: %dx%d", xThumbnail, yThumbnail);
}
}
private static class JFXX {
public static final int JPEG = 0x10;
public static final int INDEXED = 0x11;
public static final int RGB = 0x13;
private final int extensionCode;
private final byte[] thumbnail;
public JFXX(int extensionCode, byte[] thumbnail) {
this.extensionCode = extensionCode;
this.thumbnail = thumbnail;
}
@Override
public String toString() {
return String.format("JFXX extension (%s thumb size: %d)", extensionAsString(), thumbnail.length);
}
private String extensionAsString() {
switch (extensionCode) {
case JPEG:
return "JPEG";
case INDEXED:
return "Indexed";
case RGB:
return "RGB";
default:
return String.valueOf(extensionCode);
}
}
}
private static class AdobeDCT { private static class AdobeDCT {
public static final int Unknown = 0; public static final int Unknown = 0;
public static final int YCC = 1; public static final int YCC = 1;
@ -991,7 +1322,7 @@ public class JPEGImageReader extends ImageReaderBase {
private final int transform; private final int transform;
public AdobeDCT(int version, int flags0, int flags1, int transform) { public AdobeDCT(int version, int flags0, int flags1, int transform) {
this.version = version; this.version = version; // 100 or 101
this.flags0 = flags0; this.flags0 = flags0;
this.flags1 = flags1; this.flags1 = flags1;
this.transform = transform; this.transform = transform;
@ -1026,101 +1357,113 @@ public class JPEGImageReader extends ImageReaderBase {
ImageReaderBase.showIt(pImage, pTitle); ImageReaderBase.showIt(pImage, pTitle);
} }
public static void main(String[] args) throws IOException { public static void main(final String[] args) throws IOException {
File file = new File(args[0]); for (final String arg : args) {
ImageInputStream input = ImageIO.createImageInputStream(file); // File file = new File(args[0]);
Iterator<ImageReader> readers = ImageIO.getImageReaders(input); File file = new File(arg);
ImageInputStream input = ImageIO.createImageInputStream(file);
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
if (!readers.hasNext()) { if (!readers.hasNext()) {
System.err.println("No reader for: " + file); System.err.println("No reader for: " + file);
System.exit(1); System.exit(1);
}
ImageReader reader = readers.next();
System.err.println("Reading using: " + reader);
reader.addIIOReadWarningListener(new IIOReadWarningListener() {
public void warningOccurred(ImageReader source, String warning) {
System.err.println("warning: " + warning);
}
});
reader.addIIOReadProgressListener(new ProgressListenerBase() {
private static final int MAX_W = 78;
int lastProgress = 0;
@Override
public void imageStarted(ImageReader source, int imageIndex) {
System.out.print("[");
} }
@Override ImageReader reader = readers.next();
public void imageProgress(ImageReader source, float percentageDone) { // System.err.println("Reading using: " + reader);
int steps = ((int) (percentageDone * MAX_W) / 100);
for (int i = lastProgress; i < steps; i++) { reader.addIIOReadWarningListener(new IIOReadWarningListener() {
System.out.print("."); public void warningOccurred(ImageReader source, String warning) {
System.err.println("Warning: " + arg + ": " + warning);
}
});
reader.addIIOReadProgressListener(new ProgressListenerBase() {
private static final int MAX_W = 78;
int lastProgress = 0;
@Override
public void imageStarted(ImageReader source, int imageIndex) {
System.out.print("[");
} }
System.out.flush(); @Override
lastProgress = steps; public void imageProgress(ImageReader source, float percentageDone) {
} int steps = ((int) (percentageDone * MAX_W) / 100);
@Override for (int i = lastProgress; i < steps; i++) {
public void imageComplete(ImageReader source) { System.out.print(".");
for (int i = lastProgress; i < MAX_W; i++) { }
System.out.print(".");
System.out.flush();
lastProgress = steps;
} }
System.out.println("]"); @Override
} public void imageComplete(ImageReader source) {
}); for (int i = lastProgress; i < MAX_W; i++) {
System.out.print(".");
}
reader.setInput(input); System.out.println("]");
}
});
try {
ImageReadParam param = reader.getDefaultReadParam();
if (args.length > 1) {
int sub = Integer.parseInt(args[1]);
param.setSourceSubsampling(sub, sub, 0, 0);
}
long start = System.currentTimeMillis(); reader.setInput(input);
BufferedImage image = reader.read(0, param);
System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms"); try {
System.err.println("image: " + image); ImageReadParam param = reader.getDefaultReadParam();
// if (args.length > 1) {
// int sub = Integer.parseInt(args[1]);
// int sub = 4;
// param.setSourceSubsampling(sub, sub, 0, 0);
// }
long start = System.currentTimeMillis();
BufferedImage image = reader.read(0, param);
// System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");
// System.err.println("image: " + image);
// image = new ResampleOp(reader.getWidth(0) / 4, reader.getHeight(0) / 4, ResampleOp.FILTER_LANCZOS).filter(image, null); // image = new ResampleOp(reader.getWidth(0) / 4, reader.getHeight(0) / 4, ResampleOp.FILTER_LANCZOS).filter(image, null);
int maxW = 1280; // int maxW = 1280;
int maxH = 800; // int maxH = 800;
if (image.getWidth() > maxW || image.getHeight() > maxH) { int maxW = 400;
start = System.currentTimeMillis(); int maxH = 400;
float aspect = reader.getAspectRatio(0); if (image.getWidth() > maxW || image.getHeight() > maxH) {
if (aspect >= 1f) { start = System.currentTimeMillis();
image = ImageUtil.createResampled(image, maxW, Math.round(maxW / aspect), Image.SCALE_DEFAULT); float aspect = reader.getAspectRatio(0);
if (aspect >= 1f) {
image = ImageUtil.createResampled(image, maxW, Math.round(maxW / aspect), Image.SCALE_DEFAULT);
}
else {
image = ImageUtil.createResampled(image, Math.round(maxH * aspect), maxH, Image.SCALE_DEFAULT);
}
// System.err.println("Scale time: " + (System.currentTimeMillis() - start) + " ms");
} }
else {
image = ImageUtil.createResampled(image, Math.round(maxH * aspect), maxH, Image.SCALE_DEFAULT);
}
System.err.println("Scale time: " + (System.currentTimeMillis() - start) + " ms");
}
showIt(image, String.format("Image: %s [%d x %d]", file.getName(), reader.getWidth(0), reader.getHeight(0))); showIt(image, String.format("Image: %s [%d x %d]", file.getName(), reader.getWidth(0), reader.getHeight(0)));
try { try {
int numThumbnails = reader.getNumThumbnails(0); int numThumbnails = reader.getNumThumbnails(0);
for (int i = 0; i < numThumbnails; i++) { for (int i = 0; i < numThumbnails; i++) {
BufferedImage thumbnail = reader.readThumbnail(0, i); BufferedImage thumbnail = reader.readThumbnail(0, i);
showIt(thumbnail, String.format("Image: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight())); showIt(thumbnail, String.format("Thumbnail: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight()));
}
}
catch (IIOException e) {
System.err.println("Could not read thumbnails: " + e.getMessage());
e.printStackTrace();
} }
} }
catch (IIOException e) { catch (Throwable t) {
System.err.println("Could not read thumbnails: " + e.getMessage()); System.err.println(file);
e.printStackTrace(); t.printStackTrace();
}
finally {
input.close();
} }
}
finally {
input.close();
} }
} }
} }

View File

@ -0,0 +1,238 @@
/*
* 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 "TwelveMonkeys" 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 OWNER 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 javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageInputStreamImpl;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static com.twelvemonkeys.lang.Validate.notNull;
/**
* JPEGSegmentImageInputStream.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: JPEGSegmentImageInputStream.java,v 1.0 30.01.12 16:15 haraldk Exp$
*/
final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
// TODO: Rewrite JPEGSegment (from metadata) to store stream pos/length, and be able to replay data, and use instead of Segment?
// TODO: Change order of segments, to make sure APP0/JFIF is always before APP14/Adobe?
// TODO: Insert fake APP0/JFIF if needed by the reader?
final private ImageInputStream stream;
private final List<Segment> segments = new ArrayList<Segment>(64);
private int currentSegment = -1;
private Segment segment;
JPEGSegmentImageInputStream(final ImageInputStream stream) {
this.stream = notNull(stream, "stream");
}
private Segment fetchSegment() throws IOException {
// Stream init
if (currentSegment == -1) {
streamInit();
}
else {
segment = segments.get(currentSegment);
}
if (streamPos >= segment.end()) {
// Go forward in cache
while (++currentSegment < segments.size()) {
segment = segments.get(currentSegment);
if (streamPos >= segment.start && streamPos < segment.end()) {
stream.seek(segment.realStart + streamPos - segment.start);
return segment;
}
}
stream.seek(segment.realEnd());
// Scan forward
while (true) {
long realPosition = stream.getStreamPosition();
int marker = stream.readUnsignedShort();
// TODO: Refactor to make various segments optional, we probably only want the "Adobe" APP14 segment
if (isAppSegmentMarker(marker) && marker != JPEG.APP0 && marker != JPEG.APP14) {
int length = stream.readUnsignedShort(); // Length including length field itself
stream.seek(realPosition + 2 + length); // Skip marker (2) + length
}
else {
if (marker == JPEG.EOI) {
segment = new Segment(marker, realPosition, segment.end(), 2);
segments.add(segment);
}
else {
int length = stream.readUnsignedShort(); // Length including length field itself
segment = new Segment(marker, realPosition, segment.end(), 2 + length);
segments.add(segment);
}
currentSegment = segments.size() - 1;
if (streamPos >= segment.start && streamPos < segment.end()) {
stream.seek(segment.realStart + streamPos - segment.start);
break;
}
else {
stream.seek(segment.realEnd());
// Else continue forward scan
}
}
}
}
else if (streamPos < segment.start) {
// Go back in cache
while (--currentSegment >= 0) {
segment = segments.get(currentSegment);
if (streamPos >= segment.start && streamPos < segment.end()) {
stream.seek(segment.realStart + streamPos - segment.start);
break;
}
}
}
else {
stream.seek(segment.realStart + streamPos - segment.start);
}
return segment;
}
private void streamInit() throws IOException {
stream.seek(0);
int soi = stream.readUnsignedShort();
if (soi != JPEG.SOI) {
throw new IIOException(String.format("Not a JPEG stream (starts with: 0x%04x, expected SOI: 0x%04x)", soi, JPEG.SOI));
}
else {
segment = new Segment(soi, 0, 0, 2);
segments.add(segment);
currentSegment = segments.size() - 1; // 0
}
}
static boolean isAppSegmentMarker(final int marker) {
return marker >= JPEG.APP0 && marker <= JPEG.APP15;
}
private void repositionAsNecessary() throws IOException {
if (segment == null || streamPos < segment.start || streamPos >= segment.end()) {
fetchSegment();
}
}
@Override
public int read() throws IOException {
bitOffset = 0;
repositionAsNecessary();
int read = stream.read();
if (read != -1) {
streamPos++;
}
return read;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
bitOffset = 0;
// NOTE: There is a bug in the JPEGMetadata constructor (JPEGBuffer.loadBuf() method) that expects read to
// always read len bytes. Therefore, this is more complicated than it needs to... :-/
int total = 0;
while (total < len) {
repositionAsNecessary();
int count = stream.read(b, off + total, (int) Math.min(len - total, segment.end() - streamPos));
if (count == -1) {
// EOF
if (total == 0) {
return -1;
}
break;
}
else {
streamPos += count;
total += count;
}
}
return total;
}
static class Segment {
private final int marker;
final long realStart;
final long start;
final long length;
Segment(int marker, long realStart, long start, long length) {
this.marker = marker;
this.realStart = realStart;
this.start = start;
this.length = length;
}
long realEnd() {
return realStart + length;
}
long end() {
return start + length;
}
@Override
public String toString() {
return String.format("0x%04x[%d-%d]", marker, realStart, realEnd());
}
}
}

View File

@ -66,8 +66,11 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
new TestData(getClassLoaderResource("/jpeg/cmm-exception-srgb.jpg"), new Dimension(1800, 1200)), new TestData(getClassLoaderResource("/jpeg/cmm-exception-srgb.jpg"), new Dimension(1800, 1200)),
new TestData(getClassLoaderResource("/jpeg/gray-sample.jpg"), new Dimension(386, 396)), new TestData(getClassLoaderResource("/jpeg/gray-sample.jpg"), new Dimension(386, 396)),
new TestData(getClassLoaderResource("/jpeg/cmyk-sample.jpg"), new Dimension(160, 227)), new TestData(getClassLoaderResource("/jpeg/cmyk-sample.jpg"), new Dimension(160, 227)),
new TestData(getClassLoaderResource("/jpeg/cmyk-sample-multiple-chunk-icc.jpg"), new Dimension(2707, 3804)) new TestData(getClassLoaderResource("/jpeg/cmyk-sample-multiple-chunk-icc.jpg"), new Dimension(2707, 3804)),
new TestData(getClassLoaderResource("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg"), new Dimension(640, 480))
); );
// More test data in specific tests below
} }
@Override @Override
@ -111,18 +114,12 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
return Arrays.asList("image/jpeg"); return Arrays.asList("image/jpeg");
} }
/*
@Ignore("TODO: This method currently fails, fix it")
@Override
public void testSetDestinationType() throws IOException {
// TODO: This method currently fails, fix it
super.testSetDestinationType();
}
*/
// TODO: Test that subsampling is actually reading something // TODO: Test that subsampling is actually reading something
// Special cases found in the wild below
@Test @Test
public void testICCProfileClassOutputColors() throws IOException { public void testICCProfileCMYKClassOutputColors() throws IOException {
// Make sure ICC profile with class output isn't converted to too bright values // Make sure ICC profile with class output isn't converted to too bright values
JPEGImageReader reader = createReader(); JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/cmyk-sample-custom-icc-bright.jpg"))); reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/cmyk-sample-custom-icc-bright.jpg")));
@ -143,4 +140,181 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
assertEquals(expectedData[i], data[i], 5); assertEquals(expectedData[i], data[i], 5);
} }
} }
@Test
public void testICCDuplicateSequence() throws IOException {
// Variation of the above, file contains multiple ICC chunks, with all counts and sequence numbers == 1
// TODO: As the IIOException is thrown even from the readRaster method (ends up in readImageHeader native
// method), we could probably intercept at the byte/stream level, and insert correct count/sequence numbers,
// as seen by the native code.
// Should be doable, but will make reading slower. We want to avoid that in the common case.
JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-internal-kodak-srgb-jfif.jpg")));
assertEquals(345, reader.getWidth(0));
assertEquals(540, reader.getHeight(0));
BufferedImage image = reader.read(0);
assertNotNull(image);
assertEquals(345, image.getWidth());
assertEquals(540, image.getHeight());
}
@Test
public void testICCDuplicateSequenceZeroBased() throws IOException {
// File contains multiple ICC chunks, with all counts and sequence numbers == 0
// TODO: As the IIOException is thrown even from the readRaster method (ends up in readImageHeader native
// method), we could probably intercept at the byte/stream level, and insert correct count/sequence numbers,
// as seen by the native code.
// Should be doable, but will make reading slower. We want to avoid that in the common case.
JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-xerox-dc250-heavyweight-1-progressive-jfif.jpg")));
assertEquals(3874, reader.getWidth(0));
assertEquals(5480, reader.getHeight(0));
ImageReadParam param = reader.getDefaultReadParam();
param.setSourceRegion(new Rectangle(0, 0, 3874, 16)); // Save some memory
BufferedImage image = reader.read(0, param);
assertNotNull(image);
assertEquals(3874, image.getWidth());
assertEquals(16, image.getHeight());
}
@Test
public void testCCOIllegalArgument() throws IOException {
// File contains CMYK ICC profile ("Coated FOGRA27 (ISO 12647-2:2004)"), but image data is 3 channel YCC/RGB
// JFIF 1.1 with unknown origin.
JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/cco-illegalargument-rgb-coated-fogra27.jpg")));
assertEquals(281, reader.getWidth(0));
assertEquals(449, reader.getHeight(0));
BufferedImage image = reader.read(0);
assertNotNull(image);
assertEquals(281, image.getWidth());
assertEquals(449, image.getHeight());
// TODO: Need to test colors!
}
@Test
public void testNoImageTypesRGBWithCMYKProfile() throws IOException {
// File contains CMYK ICC profile ("U.S. Web Coated (SWOP) v2") AND Adobe App14 specifying YCCK conversion (!),
// but image data is plain 3 channel YCC/RGB.
// EXIF/TIFF metadata says Software: "Microsoft Windows Photo Gallery 6.0.6001.18000"...
JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/no-image-types-rgb-us-web-coated-v2-ms-photogallery-exif.jpg")));
assertEquals(1743, reader.getWidth(0));
assertEquals(2551, reader.getHeight(0));
ImageReadParam param = reader.getDefaultReadParam();
param.setSourceRegion(new Rectangle(0, 0, 1743, 16)); // Save some memory
BufferedImage image = reader.read(0, param);
assertNotNull(image);
assertEquals(1743, image.getWidth());
assertEquals(16, image.getHeight());
// TODO: Need to test colors!
assertTrue(reader.hasThumbnails(0)); // Should not blow up!
}
@Test
public void testWarningEmbeddedColorProfileInvalidIgnored() throws IOException {
JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/warning-embedded-color-profile-invalid-ignored-cmyk.jpg")));
assertEquals(183, reader.getWidth(0));
assertEquals(283, reader.getHeight(0));
BufferedImage image = reader.read(0);
assertNotNull(image);
assertEquals(183, image.getWidth());
assertEquals(283, image.getHeight());
// TODO: Need to test colors!
}
@Test
public void testHasThumbnailNoIFD1() throws IOException {
JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/srgb-exif-no-ifd1.jpg")));
assertEquals(150, reader.getWidth(0));
assertEquals(207, reader.getHeight(0));
BufferedImage image = reader.read(0);
assertNotNull(image);
assertEquals(150, image.getWidth());
assertEquals(207, image.getHeight());
assertFalse(reader.hasThumbnails(0)); // Should just not blow up, even if the EXIF IFD1 is missing
}
// TODO: Test JFIF raw thumbnail
// TODO: Test JFXX indexed thumbnail
// TODO: Test JFXX RGB thumbnail
@Test
public void testJFXXJPEGThumbnail() throws IOException {
// JFIF with JFXX JPEG encoded thumbnail
JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg")));
assertTrue(reader.hasThumbnails(0));
assertEquals(1, reader.getNumThumbnails(0));
assertEquals(80, reader.getThumbnailWidth(0, 0));
assertEquals(60, reader.getThumbnailHeight(0, 0));
BufferedImage thumbnail = reader.readThumbnail(0, 0);
assertNotNull(thumbnail);
assertEquals(80, thumbnail.getWidth());
assertEquals(60, thumbnail.getHeight());
}
@Test
public void testEXIFJPEGThumbnail() throws IOException {
JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/cmyk-sample-multiple-chunk-icc.jpg")));
assertTrue(reader.hasThumbnails(0));
assertEquals(1, reader.getNumThumbnails(0));
assertEquals(114, reader.getThumbnailWidth(0, 0));
assertEquals(160, reader.getThumbnailHeight(0, 0));
BufferedImage thumbnail = reader.readThumbnail(0, 0);
assertNotNull(thumbnail);
assertEquals(114, thumbnail.getWidth());
assertEquals(160, thumbnail.getHeight());
}
@Test
public void testEXIFRAWThumbnail() throws IOException {
JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/exif-rgb-thumbnail-sony-d700.jpg")));
assertTrue(reader.hasThumbnails(0));
assertEquals(1, reader.getNumThumbnails(0));
assertEquals(80, reader.getThumbnailWidth(0, 0));
assertEquals(60, reader.getThumbnailHeight(0, 0));
BufferedImage thumbnail = reader.readThumbnail(0, 0);
assertNotNull(thumbnail);
assertEquals(80, thumbnail.getWidth());
assertEquals(60, thumbnail.getHeight());
}
} }

View File

@ -0,0 +1,123 @@
/*
* 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 "TwelveMonkeys" 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 OWNER 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.metadata.jpeg.JPEGSegment;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi;
import org.junit.Test;
import org.mockito.internal.matchers.LessOrEqual;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.spi.IIORegistry;
import javax.imageio.stream.ImageInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import static org.junit.Assert.*;
/**
* JPEGSegmentImageInputStreamTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: JPEGSegmentImageInputStreamTest.java,v 1.0 30.01.12 22:14 haraldk Exp$
*/
public class JPEGSegmentImageInputStreamTest {
static {
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
}
protected URL getClassLoaderResource(final String pName) {
return getClass().getResource(pName);
}
@Test(expected = IllegalArgumentException.class)
public void testCreateNull() {
new JPEGSegmentImageInputStream(null);
}
@Test(expected = IIOException.class)
public void testStreamNonJPEG() throws IOException {
ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(new ByteArrayInputStream(new byte[] {42, 42, 0, 0, 77, 99})));
stream.read();
}
@Test
public void testStreamRealData() throws IOException {
ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-internal-kodak-srgb-jfif.jpg")));
assertEquals(JPEG.SOI, stream.readUnsignedShort());
assertEquals(JPEG.APP0, stream.readUnsignedShort());
}
@Test
public void testStreamRealDataArray() throws IOException {
ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/invalid-icc-duplicate-sequence-numbers-rgb-internal-kodak-srgb-jfif.jpg")));
byte[] bytes = new byte[20];
// NOTE: read(byte[], int, int) must always read len bytes (or until EOF), due to known bug in Sun code
assertEquals(20, stream.read(bytes, 0, 20));
assertArrayEquals(new byte[] {(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xE0, 0x0, 0x10, 'J', 'F', 'I', 'F', 0x0, 0x1, 0x1, 0x1, 0x1, (byte) 0xCC, 0x1, (byte) 0xCC, 0, 0}, bytes);
}
@Test
public void testStreamRealDataLength() throws IOException {
ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/cmm-exception-adobe-rgb.jpg")));
long length = 0;
while (stream.read() != -1) {
length++;
}
assertThat(length, new LessOrEqual<Long>(10203l)); // In no case should length increase
assertEquals(9495l, length); // May change, if more chunks are passed to reader...
}
@Test
public void testAppSegmentsFiltering() throws IOException {
ImageInputStream stream = new JPEGSegmentImageInputStream(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/no-image-types-rgb-us-web-coated-v2-ms-photogallery-exif.jpg")));
List<JPEGSegment> appSegments = JPEGSegmentUtil.readSegments(stream, JPEGSegmentUtil.APP_SEGMENTS);
assertEquals(2, appSegments.size());
assertEquals(JPEG.APP0, appSegments.get(0).marker());
assertEquals("JFIF", appSegments.get(0).identifier());
assertEquals(JPEG.APP14, appSegments.get(1).marker());
assertEquals("Adobe", appSegments.get(1).identifier());
// And thus, no Exif, no ICC_PROFILE or other segments
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -0,0 +1,5 @@
The test files in this folder may contain copyrighted artwork. However, I believe that using them for test purposes
(without actually displaying the artwork) must be considered fair use.
If you disagree for any reason, please send me a note, and I will remove your image from the distribution.
-- harald.kuhr@gmail.com

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB