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.
@ -29,13 +29,18 @@
|
||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
import com.twelvemonkeys.image.ImageUtil;
|
||||
import com.twelvemonkeys.image.InverseColorMapIndexColorModel;
|
||||
import com.twelvemonkeys.imageio.ImageReaderBase;
|
||||
import com.twelvemonkeys.imageio.color.ColorSpaces;
|
||||
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
|
||||
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.TIFF;
|
||||
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.util.IIOUtil;
|
||||
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
|
||||
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$
|
||||
*/
|
||||
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"));
|
||||
|
||||
@ -107,11 +113,14 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
/** Our JPEG reading delegate */
|
||||
private final ImageReader delegate;
|
||||
|
||||
/** Listens to progress updates in the delegate, and delegates back to this instance */
|
||||
private final ProgressDelegator progressDelegator;
|
||||
|
||||
/** Cached JPEG app segments */
|
||||
private List<JPEGSegment> segments;
|
||||
|
||||
private List<BufferedImage> thumbnails;
|
||||
|
||||
JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) {
|
||||
super(provider);
|
||||
this.delegate = Validate.notNull(delegate);
|
||||
@ -125,12 +134,11 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
delegate.addIIOReadWarningListener(progressDelegator);
|
||||
}
|
||||
|
||||
// TODO: Delegate all methods?!
|
||||
|
||||
@Override
|
||||
protected void resetMembers() {
|
||||
delegate.reset();
|
||||
segments = null;
|
||||
thumbnails = null;
|
||||
|
||||
installListeners();
|
||||
}
|
||||
@ -182,7 +190,6 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
// + original color profile should be an option
|
||||
|
||||
).iterator();
|
||||
|
||||
}
|
||||
|
||||
return types;
|
||||
@ -198,7 +205,8 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
public void setInput(Object input, boolean seekForwardOnly, boolean 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
|
||||
@ -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
|
||||
|
||||
SOF startOfFrame = getSOF();
|
||||
@ -312,6 +320,15 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
// TODO: Move to getImageTypes + add native color space if profile != null
|
||||
).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...
|
||||
|
||||
BufferedImage image = getDestination(param, imageTypes, origWidth, origHeight);
|
||||
@ -331,8 +348,28 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
convert = new ColorConvertOp(srcCs, image.getColorModel().getColorSpace(), 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
|
||||
if (replacement != image.getColorModel().getColorSpace()) {
|
||||
else if (replacement != image.getColorModel().getColorSpace()) {
|
||||
// TODO: Use profiles instead of CS, if ICC profiles? Avoid creating expensive CS.
|
||||
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);
|
||||
// }
|
||||
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
|
||||
@ -424,11 +461,11 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// NOTE: Would be cleaner to clone the param, unfortunately it can't be done easily...
|
||||
param.setSourceRegion(origSourceRegion);
|
||||
|
||||
// Restore normal read progress processing
|
||||
progressDelegator.resetProgressRange();
|
||||
|
||||
// NOTE: Would be cleaner to clone the param, unfortunately it can't be done easily...
|
||||
param.setSourceRegion(origSourceRegion);
|
||||
}
|
||||
|
||||
processImageComplete();
|
||||
@ -437,8 +474,8 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
}
|
||||
|
||||
private ICC_Profile ensureDisplayProfile(final ICC_Profile profile) {
|
||||
// TODO: This is probably not the right way to do it... :-P
|
||||
// TODO: Consider moving to ColorSpaces class or new class in imageio.color package
|
||||
// NOTE: This is probably not the right way to do it... :-P
|
||||
// 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,
|
||||
// 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");
|
||||
|
||||
if (!exifSegments.isEmpty()) {
|
||||
@ -505,16 +542,77 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
|
||||
ImageInputStream stream = ImageIO.createImageInputStream(data);
|
||||
|
||||
@SuppressWarnings("UnnecessaryLocalVariable")
|
||||
Directory exifMetadata = new EXIFReader().read(stream);
|
||||
CompoundDirectory exifMetadata = (CompoundDirectory) 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);
|
||||
|
||||
if (width == null || height == null) {
|
||||
throw new IIOException("Missing dimensions for RAW EXIF thumbnail");
|
||||
}
|
||||
|
||||
Entry bitsPerSample = ifd1.getEntryById(TIFF.TAG_BITS_PER_SAMPLE);
|
||||
Entry samplesPerPixel = ifd1.getEntryById(TIFF.TAG_SAMPLES_PER_PIXELS);
|
||||
Entry photometricInterpretation = ifd1.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION);
|
||||
|
||||
// Entry jpegOffset = exifMetadata.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT);
|
||||
// if (jpegOffset != null) {
|
||||
// stream.seek((Long) jpegOffset.getValue());
|
||||
// BufferedImage image = ImageIO.read(IIOUtil.createStreamAdapter(stream));
|
||||
// System.err.println("image: " + image);
|
||||
// showIt(image, "Thumbnail");
|
||||
// }
|
||||
// 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;
|
||||
}
|
||||
@ -540,10 +638,6 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
return appSegments;
|
||||
}
|
||||
|
||||
public boolean isJFIFAPP0Present() throws IOException {
|
||||
return !(getAppSegments(JPEG.APP0, "JFIF").isEmpty() && getAppSegments(JPEG.APP0, "JFXX").isEmpty());
|
||||
}
|
||||
|
||||
private SOF getSOF() throws IOException {
|
||||
for (JPEGSegment segment : segments) {
|
||||
if (JPEG.SOF0 <= segment.marker() && segment.marker() <= JPEG.SOF3 ||
|
||||
@ -581,6 +675,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
if (!adobe.isEmpty()) {
|
||||
@ -598,6 +693,58 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
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 {
|
||||
// ICC v 1.42 (2006) annex B:
|
||||
// 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 chunkCount = stream.readUnsignedByte();
|
||||
|
||||
InputStream[] streams = new InputStream[chunkCount];
|
||||
streams[chunkNumber - 1] = stream;
|
||||
// Some weird JPEGs use 0-based indexes... count == 0 and all numbers == 0.
|
||||
// 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());
|
||||
|
||||
chunkNumber = stream.readUnsignedByte();
|
||||
if (stream.readUnsignedByte() != chunkCount) {
|
||||
|
||||
if (!badICC && stream.readUnsignedByte() != chunkCount) {
|
||||
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))));
|
||||
@ -668,32 +830,121 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
// TODO: Fix thumbnails based on JFIF and EXIF thumbnails
|
||||
@Override
|
||||
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
|
||||
public boolean hasThumbnails(int imageIndex) throws IOException {
|
||||
return delegate.hasThumbnails(imageIndex);
|
||||
public int getNumThumbnails(final int imageIndex) throws IOException {
|
||||
readThumbnailMetadata(imageIndex);
|
||||
|
||||
return thumbnails.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumThumbnails(int imageIndex) throws IOException {
|
||||
return delegate.getNumThumbnails(imageIndex);
|
||||
private void checkThumbnailBounds(int imageIndex, int thumbnailIndex) throws IOException {
|
||||
Validate.isTrue(thumbnailIndex >= 0, thumbnailIndex, "thumbnailIndex < 0; %d");
|
||||
Validate.isTrue(getNumThumbnails(imageIndex) > thumbnailIndex, thumbnailIndex, "thumbnailIndex >= numThumbnails; %d");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getThumbnailWidth(int imageIndex, int thumbnailIndex) throws IOException {
|
||||
return delegate.getThumbnailWidth(imageIndex, thumbnailIndex);
|
||||
checkThumbnailBounds(imageIndex, thumbnailIndex);
|
||||
|
||||
return thumbnails.get(thumbnailIndex).getWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getThumbnailHeight(int imageIndex, int thumbnailIndex) throws IOException {
|
||||
return delegate.getThumbnailHeight(imageIndex, thumbnailIndex);
|
||||
checkThumbnailBounds(imageIndex, thumbnailIndex);
|
||||
|
||||
return thumbnails.get(thumbnailIndex).getHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
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) {
|
||||
@ -910,7 +1161,6 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
public void warningOccurred(ImageReader source, String warning) {
|
||||
processWarningOccurred(warning);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
public static final int Unknown = 0;
|
||||
public static final int YCC = 1;
|
||||
@ -991,7 +1322,7 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
private final 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.flags1 = flags1;
|
||||
this.transform = transform;
|
||||
@ -1026,101 +1357,113 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
ImageReaderBase.showIt(pImage, pTitle);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
File file = new File(args[0]);
|
||||
ImageInputStream input = ImageIO.createImageInputStream(file);
|
||||
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
|
||||
public static void main(final String[] args) throws IOException {
|
||||
for (final String arg : args) {
|
||||
// File file = new File(args[0]);
|
||||
File file = new File(arg);
|
||||
ImageInputStream input = ImageIO.createImageInputStream(file);
|
||||
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
|
||||
|
||||
if (!readers.hasNext()) {
|
||||
System.err.println("No reader for: " + file);
|
||||
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("[");
|
||||
if (!readers.hasNext()) {
|
||||
System.err.println("No reader for: " + file);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void imageProgress(ImageReader source, float percentageDone) {
|
||||
int steps = ((int) (percentageDone * MAX_W) / 100);
|
||||
ImageReader reader = readers.next();
|
||||
// System.err.println("Reading using: " + reader);
|
||||
|
||||
for (int i = lastProgress; i < steps; i++) {
|
||||
System.out.print(".");
|
||||
reader.addIIOReadWarningListener(new IIOReadWarningListener() {
|
||||
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();
|
||||
lastProgress = steps;
|
||||
}
|
||||
@Override
|
||||
public void imageProgress(ImageReader source, float percentageDone) {
|
||||
int steps = ((int) (percentageDone * MAX_W) / 100);
|
||||
|
||||
@Override
|
||||
public void imageComplete(ImageReader source) {
|
||||
for (int i = lastProgress; i < MAX_W; i++) {
|
||||
System.out.print(".");
|
||||
for (int i = lastProgress; i < steps; 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();
|
||||
BufferedImage image = reader.read(0, param);
|
||||
System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");
|
||||
System.err.println("image: " + image);
|
||||
reader.setInput(input);
|
||||
|
||||
try {
|
||||
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);
|
||||
|
||||
int maxW = 1280;
|
||||
int maxH = 800;
|
||||
if (image.getWidth() > maxW || image.getHeight() > maxH) {
|
||||
start = System.currentTimeMillis();
|
||||
float aspect = reader.getAspectRatio(0);
|
||||
if (aspect >= 1f) {
|
||||
image = ImageUtil.createResampled(image, maxW, Math.round(maxW / aspect), Image.SCALE_DEFAULT);
|
||||
// int maxW = 1280;
|
||||
// int maxH = 800;
|
||||
int maxW = 400;
|
||||
int maxH = 400;
|
||||
if (image.getWidth() > maxW || image.getHeight() > maxH) {
|
||||
start = System.currentTimeMillis();
|
||||
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 {
|
||||
int numThumbnails = reader.getNumThumbnails(0);
|
||||
for (int i = 0; i < numThumbnails; i++) {
|
||||
BufferedImage thumbnail = reader.readThumbnail(0, i);
|
||||
showIt(thumbnail, String.format("Image: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight()));
|
||||
try {
|
||||
int numThumbnails = reader.getNumThumbnails(0);
|
||||
for (int i = 0; i < numThumbnails; i++) {
|
||||
BufferedImage thumbnail = reader.readThumbnail(0, i);
|
||||
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) {
|
||||
System.err.println("Could not read thumbnails: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
catch (Throwable t) {
|
||||
System.err.println(file);
|
||||
t.printStackTrace();
|
||||
}
|
||||
finally {
|
||||
input.close();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
input.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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/gray-sample.jpg"), new Dimension(386, 396)),
|
||||
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
|
||||
@ -111,18 +114,12 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
|
||||
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
|
||||
|
||||
// Special cases found in the wild below
|
||||
|
||||
@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
|
||||
JPEGImageReader reader = createReader();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 553 KiB |
After Width: | Height: | Size: 78 KiB |
@ -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
|
After Width: | Height: | Size: 221 KiB |
After Width: | Height: | Size: 5.7 MiB |
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 1.7 MiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 40 KiB |