TMI-21: Implemented getRawImageType and getImageTypes for CMYK/YCCK.

TMI-16: Refactorings, cleaner color space determination + tests for thumbnail readers.
This commit is contained in:
Harald Kuhr 2012-05-07 20:26:26 +02:00
parent aaef2e4fad
commit a4dfb7a009
17 changed files with 960 additions and 241 deletions

View File

@ -29,12 +29,12 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
/**
* AdobeDCT
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: AdobeDCT.java,v 1.0 23.04.12 16:55 haraldk Exp$
*/
* AdobeDCT
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: AdobeDCT.java,v 1.0 23.04.12 16:55 haraldk Exp$
*/
class AdobeDCT {
public static final int Unknown = 0;
public static final int YCC = 1;

View File

@ -57,8 +57,8 @@ final class EXIFThumbnailReader extends ThumbnailReader {
private transient SoftReference<BufferedImage> cachedThumbnail;
public EXIFThumbnailReader(JPEGImageReader parent, int imageIndex, int thumbnailIndex, Directory ifd, ImageInputStream stream) {
super(parent, imageIndex, thumbnailIndex);
public EXIFThumbnailReader(ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, Directory ifd, ImageInputStream stream) {
super(progressListener, imageIndex, thumbnailIndex);
this.ifd = ifd;
this.stream = stream;
@ -112,6 +112,7 @@ final class EXIFThumbnailReader extends ThumbnailReader {
// For certain EXIF files (encoded with TIFF.TAG_YCBCR_POSITIONING = 2?), we need
// EXIF information to read the thumbnail correctly (otherwise the colors are messed up).
// Probably related to: http://bugs.sun.com/view_bug.do?bug_id=4881314
// HACK: Splice empty EXIF information into the thumbnail stream
byte[] fakeEmptyExif = {

View File

@ -28,13 +28,17 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* JFIFSegment
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: JFIFSegment.java,v 1.0 23.04.12 16:52 haraldk Exp$
*/
* JFIFSegment
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: JFIFSegment.java,v 1.0 23.04.12 16:52 haraldk Exp$
*/
class JFIFSegment {
final int majorVersion;
final int minorVersion;
@ -45,7 +49,7 @@ class JFIFSegment {
final int yThumbnail;
final byte[] thumbnail;
public JFIFSegment(int majorVersion, int minorVersion, int units, int xDensity, int yDensity, int xThumbnail, int yThumbnail, byte[] thumbnail) {
private JFIFSegment(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;
@ -81,4 +85,21 @@ class JFIFSegment {
return String.format("thumbnail: %dx%d", xThumbnail, yThumbnail);
}
public static JFIFSegment read(final InputStream data) throws IOException {
DataInputStream stream = new DataInputStream(data);
int x, y;
return new JFIFSegment(
stream.readUnsignedByte(),
stream.readUnsignedByte(),
stream.readUnsignedByte(),
stream.readUnsignedShort(),
stream.readUnsignedShort(),
x = stream.readUnsignedByte(),
y = stream.readUnsignedByte(),
JPEGImageReader.readFully(stream, x * y * 3)
);
}
}

View File

@ -41,8 +41,8 @@ import java.io.IOException;
final class JFIFThumbnailReader extends ThumbnailReader {
private final JFIFSegment segment;
public JFIFThumbnailReader(JPEGImageReader parent, int imageIndex, int thumbnailIndex, JFIFSegment segment) {
super(parent, imageIndex, thumbnailIndex);
public JFIFThumbnailReader(ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, JFIFSegment segment) {
super(progressListener, imageIndex, thumbnailIndex);
this.segment = segment;
}
@ -50,7 +50,6 @@ final class JFIFThumbnailReader extends ThumbnailReader {
public BufferedImage read() {
processThumbnailStarted();
BufferedImage thumbnail = readRawThumbnail(segment.thumbnail, segment.thumbnail.length, 0, segment.xThumbnail, segment.yThumbnail);
processThumbnailProgress(100f);
processThumbnailComplete();

View File

@ -28,13 +28,17 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* JFXXSegment
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: JFXXSegment.java,v 1.0 23.04.12 16:54 haraldk Exp$
*/
* JFXXSegment
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: JFXXSegment.java,v 1.0 23.04.12 16:54 haraldk Exp$
*/
class JFXXSegment {
public static final int JPEG = 0x10;
public static final int INDEXED = 0x11;
@ -43,7 +47,7 @@ class JFXXSegment {
final int extensionCode;
final byte[] thumbnail;
public JFXXSegment(int extensionCode, byte[] thumbnail) {
private JFXXSegment(int extensionCode, byte[] thumbnail) {
this.extensionCode = extensionCode;
this.thumbnail = thumbnail;
}
@ -65,4 +69,13 @@ class JFXXSegment {
return String.valueOf(extensionCode);
}
}
public static JFXXSegment read(InputStream data, int length) throws IOException {
DataInputStream stream = new DataInputStream(data);
return new JFXXSegment(
stream.readUnsignedByte(),
JPEGImageReader.readFully(stream, length - 1)
);
}
}

View File

@ -49,8 +49,8 @@ final class JFXXThumbnailReader extends ThumbnailReader {
private transient SoftReference<BufferedImage> cachedThumbnail;
protected JFXXThumbnailReader(final JPEGImageReader parent, final int imageIndex, final int thumbnailIndex, final JFXXSegment segment) {
super(parent, imageIndex, thumbnailIndex);
protected JFXXThumbnailReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex, final JFXXSegment segment) {
super(progressListener, imageIndex, thumbnailIndex);
this.segment = segment;
}

View File

@ -0,0 +1,49 @@
/*
* 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;
/**
* JPEGColorSpace
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: JPEGColorSpace.java,v 1.0 26.04.12 15:05 haraldk Exp$
*/
enum JPEGColorSpace {
Gray,
GrayA,
RGB,
RGBA,
YCbCr,
YCbCrA,
PhotoYCC,
PhotoYCCA,
CMYK,
YCCK
}

View File

@ -58,8 +58,8 @@ import java.util.List;
/**
* A JPEG {@code ImageReader} implementation based on the JRE {@code JPEGImageReader},
* with support for CMYK JPEGs and other non-standard color spaces,
* like embedded ICC color spaces with rendering intent other than 'perceptual'.
* with support for CMYK/YCCK JPEGs, non-standard color spaces,broken ICC profiles
* and more.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author LUT-based YCbCR conversion by Werner Randelshofer
@ -69,7 +69,6 @@ import java.util.List;
public class JPEGImageReader extends ImageReaderBase {
// 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.. :-/
// TODO: Split thumbnail reading into separate class(es)
private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug"));
@ -171,33 +170,78 @@ public class JPEGImageReader extends ImageReaderBase {
@Override
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
// TODO: Read header, and make sure we return valid types for the images we can now read
Iterator<ImageTypeSpecifier> types = delegate.getImageTypes(imageIndex);
JPEGColorSpace csType = getSourceCSType(getAdobeDCT(), getSOF());
ICC_Profile profile = getEmbeddedICCProfile();
AdobeDCT adobeDCT = getAdobeDCT();
if (types == null || !types.hasNext() || csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK) {
ArrayList<ImageTypeSpecifier> typeList = new ArrayList<ImageTypeSpecifier>();
// Add the standard types, we can always convert to these
typeList.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR));
typeList.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB));
typeList.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR));
// TODO: FixMe
if (types == null || !types.hasNext() || adobeDCT != null && adobeDCT.getTransform() == AdobeDCT.YCCK || profile != null && profile.getColorSpaceType() == ColorSpace.TYPE_CMYK) {
return Arrays.asList(
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR),
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB),
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR)
// We also read and return CMYK if the source image is CMYK/YCCK + original color profile if present
ICC_Profile profile = getEmbeddedICCProfile();
// TODO: We can/should also read and return it as CMYK if the source image is CMYK..
// + original color profile should be an option
if (csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK) {
if (profile != null) {
typeList.add(ImageTypeSpecifier.createInterleaved(ColorSpaces.createColorSpace(profile), new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false));
}
).iterator();
typeList.add(ImageTypeSpecifier.createInterleaved(ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK), new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false));
}
else if (csType == JPEGColorSpace.YCbCr || csType == JPEGColorSpace.RGB) {
if (profile != null) {
typeList.add(ImageTypeSpecifier.createInterleaved(ColorSpaces.createColorSpace(profile), new int[] {0, 1, 2}, DataBuffer.TYPE_BYTE, false, false));
}
}
else if (csType == JPEGColorSpace.YCbCrA || csType == JPEGColorSpace.RGBA) {
// Prepend ARGB types
typeList.addAll(0, Arrays.asList(
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB),
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR),
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB_PRE),
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR_PRE)
));
if (profile != null) {
typeList.add(ImageTypeSpecifier.createInterleaved(ColorSpaces.createColorSpace(profile), new int[] {0, 1, 2, 3}, DataBuffer.TYPE_BYTE, false, false));
}
}
return typeList.iterator();
}
return types;
}
@Override
public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException {
// TODO: Implement something better, so we don't return null for CMYK images + fixes the "Inconsistent metadata" issue
return delegate.getRawImageType(imageIndex);
public
ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException {
// If delegate can determine the spec, we'll just go with that
ImageTypeSpecifier rawType = delegate.getRawImageType(imageIndex);
if (rawType != null) {
return rawType;
}
// Otherwise, consult the image metadata
JPEGColorSpace csType = getSourceCSType(getAdobeDCT(), getSOF());
switch (csType) {
case CMYK:
// Create based on embedded profile if exists, or create from "Generic CMYK"
ICC_Profile profile = getEmbeddedICCProfile();
if (profile != null) {
return ImageTypeSpecifier.createInterleaved(ColorSpaces.createColorSpace(profile), new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false);
}
return ImageTypeSpecifier.createInterleaved(ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK), new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false);
default:
// For other types, we probably can't give a proper type, return null
return null;
}
}
@Override
@ -226,13 +270,16 @@ public class JPEGImageReader extends ImageReaderBase {
ICC_Profile profile = getEmbeddedICCProfile();
AdobeDCT adobeDCT = getAdobeDCT();
// TODO: Probably something bogus here, as ICC profile isn't applied if reading through the delegate any more...
// We need to apply ICC profile unless the profile is sRGB/default gray (whatever that is)
// - or only filter out the bad ICC profiles in the JPEGSegmentImageInputStream.
if (delegate.canReadRaster() && (
unsupported ||
adobeDCT != null && adobeDCT.getTransform() == AdobeDCT.YCCK ||
profile != null && (ColorSpaces.isOffendingColorProfile(profile) || profile.getColorSpaceType() == ColorSpace.TYPE_CMYK))) {
if (DEBUG) {
System.out.println("Reading using raster and extra conversion");
System.out.println("ICC color profile = " + profile);
System.out.println("ICC color profile: " + profile);
}
return readImageAsRasterAndReplaceColorProfile(imageIndex, param, ensureDisplayProfile(profile));
@ -249,144 +296,63 @@ public class JPEGImageReader extends ImageReaderBase {
int origWidth = getWidth(imageIndex);
int origHeight = getHeight(imageIndex);
ColorSpace srcCs = null;
/*--------------------------------------------------------------------------------------------------------------
From http://download.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html:
"When reading, the contents of the stream are interpreted by the usual JPEG conventions, as follows:
If a JFIF APP0 marker segment is present, the colorspace is known to be either grayscale or YCbCr. If an APP2
marker segment containing an embedded ICC profile is also present, then the YCbCr is converted to RGB according
to the formulas given in the JFIF spec, and the ICC profile is assumed to refer to the resulting RGB space.
If an Adobe APP14 marker segment is present, the colorspace is determined by consulting the transform flag.
The transform flag takes one of three values:
o 2 - The image is encoded as YCCK (implicitly converted from CMYK on encoding).
o 1 - The image is encoded as YCbCr (implicitly converted from RGB on encoding).
o 0 - Unknown. 3-channel images are assumed to be RGB, 4-channel images are assumed to be CMYK.
If neither marker segment is present, the following procedure is followed: Single-channel images are assumed
to be grayscale, and 2-channel images are assumed to be grayscale with an alpha channel. For 3- and 4-channel
images, the component ids are consulted. If these values are 1-3 for a 3-channel image, then the image is
assumed to be YCbCr. If these values are 1-4 for a 4-channel image, then the image is assumed to be YCbCrA. If
these values are > 4, they are checked against the ASCII codes for 'R', 'G', 'B', 'A', 'C', 'c'.
These can encode the following colorspaces:
RGB
RGBA
YCC (as 'Y','C','c'), assumed to be PhotoYCC
YCCA (as 'Y','C','c','A'), assumed to be PhotoYCCA
Otherwise, 3-channel subsampled images are assumed to be YCbCr, 3-channel non-subsampled images are assumed to
be RGB, 4-channel subsampled images are assumed to be YCCK, and 4-channel, non-subsampled images are assumed to
be CMYK.
All other images are declared uninterpretable and an exception is thrown if an attempt is made to read one as
a BufferedImage. Such an image may be read only as a Raster. If an image is interpretable but there is no Java
ColorSpace available corresponding to the encoded colorspace (e.g. YCbCr), then ImageReader.getRawImageType
will return null."
--------------------------------------------------------------------------------------------------------------*/
// 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();
AdobeDCT adobeDCT = getAdobeDCT();
SOF startOfFrame = getSOF();
JPEGColorSpace csType = getSourceCSType(adobeDCT, startOfFrame);
Iterator<ImageTypeSpecifier> imageTypes = delegate.getImageTypes(imageIndex);
int transform = adobeDCT != null ? adobeDCT.getTransform() : AdobeDCT.Unknown;
// TODO: The !types.hasNext test is broken for JDK7, as it does return types...
// CMYK Support, assuming the delegate reader can't decode, and any 4 component image is CMYK
if ((!imageTypes.hasNext() || transform == AdobeDCT.YCCK || profile != null && profile.getColorSpaceType() == ColorSpace.TYPE_CMYK) && startOfFrame.componentsInFrame == 4) {
// NOTE: Reading the metadata here chokes on some images. Instead, parse the Adobe App14 segment and read transform directly
// TODO: If cmyk and no ICC profile, just use FastCMYKToRGB, without attempting loading Generic CMYK profile first?
// TODO: Don't get generic CMYK if we already have a CMYK profile...
srcCs = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK);
imageTypes = Arrays.asList(
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR),
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB),
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR)
// TODO: Only alpha if source has alpha... (ColorConvertOp chokes otherwise)
// ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB),
// ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB_PRE),
// ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR),
// ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR_PRE)
// TODO: Move to getImageTypes + add native color space if profile != null
).iterator();
}
else if (!imageTypes.hasNext() && profile != null) {
// TODO: Merge with above?
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...
Iterator<ImageTypeSpecifier> imageTypes = getImageTypes(imageIndex);
BufferedImage image = getDestination(param, imageTypes, origWidth, origHeight);
// System.err.println("JPEGImageReader.readImageAsRasterAndReplaceColorProfile: " + image);
WritableRaster destination = image.getRaster();
// TODO: checkReadParamBandSettings(param, );
RasterOp convert = null;
ICC_ColorSpace replacement = profile != null ? ColorSpaces.createColorSpace(profile) : null;
ICC_ColorSpace intendedCS = profile != null ? ColorSpaces.createColorSpace(profile) : null;
if (profile != null && profile.getColorSpaceType() == ColorSpace.TYPE_GRAY && image.getColorModel().getColorSpace().getType() == ColorSpace.CS_GRAY) {
if (profile != null && (csType == JPEGColorSpace.Gray || csType == JPEGColorSpace.GrayA)) {
// com.sun. reader does not do ColorConvertOp for CS_GRAY, even if embedded ICC profile,
// probably because IJG native part does it already...? If applied, color looks wrong (too dark)...
convert = new ColorConvertOp(srcCs, image.getColorModel().getColorSpace(), null);
// convert = new ColorConvertOp(intendedCS, image.getColorModel().getColorSpace(), null);
}
else if (replacement != null) {
else if (intendedCS != null) {
// Handle inconsistencies
if (startOfFrame.componentsInFrame != replacement.getNumComponents()) {
if (startOfFrame.componentsInFrame < 4 && transform == AdobeDCT.YCCK) {
if (startOfFrame.componentsInFrame != intendedCS.getNumComponents()) {
if (startOfFrame.componentsInFrame < 4 && (csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK)) {
processWarningOccurred(String.format(
"Invalid Adobe App14 marker. Indicates YCCK/CMYK data, but SOF%d has %d color components. " +
"Ignoring Adobe App14 marker, assuming YCC/RGB data.",
"Ignoring Adobe App14 marker, assuming YCbCr/RGB data.",
startOfFrame.marker & 0xf, 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 SOF%d has %d color components. " +
"Ignoring ICC profile, assuming YCC/RGB data.",
replacement.getNumComponents(), startOfFrame.marker & 0xf, startOfFrame.componentsInFrame
));
srcCs = null;
csType = JPEGColorSpace.YCbCr;
}
else {
// 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 SOF%d has %d color components. " +
"Ignoring ICC profile, assuming source color space %s.",
intendedCS.getNumComponents(), startOfFrame.marker & 0xf, startOfFrame.componentsInFrame, csType
));
}
}
// NOTE: Avoid using CCOp if same color space, as it's more compatible that way
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);
else if (intendedCS != image.getColorModel().getColorSpace()) {
convert = new ColorConvertOp(intendedCS, image.getColorModel().getColorSpace(), null);
}
// Else, pass through with no conversion
}
else if (srcCs != null) {
if (!(srcCs instanceof ICC_ColorSpace) && image.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_RGB && srcCs.getType() == ColorSpace.TYPE_CMYK) {
convert = new FastCMYKToRGB();
else if (csType == JPEGColorSpace.YCCK || csType == JPEGColorSpace.CMYK) {
ColorSpace cmykCS = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK);
if (cmykCS instanceof ICC_ColorSpace) {
convert = new ColorConvertOp(cmykCS, image.getColorModel().getColorSpace(), null);
}
else {
// TODO: Use profiles instead of CS, if ICC profiles? Avoid creating expensive CS.
convert = new ColorConvertOp(srcCs, image.getColorModel().getColorSpace(), null);
// ColorConvertOp using non-ICC CS is deadly slow, fall back to fast conversion instead
convert = new FastCMYKToRGB();
}
}
// else if (!image.getColorModel().getColorSpace().isCS_sRGB()) {
// TODO: Need to handle case where src and dest differ still
// convert = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_sRGB), image.getColorModel().getColorSpace(), null);
// }
else if (profile != null) {
processWarningOccurred("Embedded ICC color profile is incompatible with Java 2D, color profile will be ignored.");
}
@ -409,10 +375,8 @@ public class JPEGImageReader extends ImageReaderBase {
processImageStarted(imageIndex);
// Unfortunately looping is slower than reading all at once, but
// that requires 2 x + memory, so a few steps is an ok compromise I guess
// that requires 2 x memory or more, so a few steps is an ok compromise I guess
try {
int srcCsType = srcCs != null ? srcCs.getType() : image.getColorModel().getColorSpace().getType();
final int step = Math.max(1024, srcRegion.height / 10); // * param.getSourceYSubsampling(); // TODO: Using a multiple of 8 is probably a good idea for JPEG
final int srcMaxY = srcRegion.y + srcRegion.height;
int destY = dstRegion.y;
@ -428,13 +392,13 @@ public class JPEGImageReader extends ImageReaderBase {
Raster raster = delegate.readRaster(imageIndex, param); // non-converted
// Apply source color conversion from implicit color space
if ((transform == AdobeDCT.YCC || transform == AdobeDCT.Unknown) && srcCsType == ColorSpace.TYPE_RGB) {
if (csType == JPEGColorSpace.YCbCr || csType == JPEGColorSpace.YCbCrA) {
YCbCrConverter.convertYCbCr2RGB(raster);
}
else if (transform == AdobeDCT.YCCK && srcCsType == ColorSpace.TYPE_CMYK) {
else if (csType == JPEGColorSpace.YCCK) {
YCbCrConverter.convertYCCK2CMYK(raster);
}
else if (transform == AdobeDCT.Unknown && srcCsType == ColorSpace.TYPE_CMYK) {
else if (csType == JPEGColorSpace.CMYK) {
invertCMYK(raster);
}
// ...else assume the raster is already converted
@ -472,6 +436,123 @@ public class JPEGImageReader extends ImageReaderBase {
return image;
}
static JPEGColorSpace getSourceCSType(AdobeDCT adobeDCT, final SOF startOfFrame) throws IIOException {
/*
ADAPTED from http://download.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html:
When reading, the contents of the stream are interpreted by the usual JPEG conventions, as follows:
If a JFIF APP0 marker segment is present, the colorspace is known to be either grayscale, YCbCr or CMYK.
If an APP2 marker segment containing an embedded ICC profile is also present, then YCbCr is converted to RGB according
to the formulas given in the JFIF spec, and the ICC profile is assumed to refer to the resulting RGB space.
CMYK data is read as is, and the ICC profile is assumed to refer to the resulting CMYK space.
If an Adobe APP14 marker segment is present, the colorspace is determined by consulting the transform flag.
The transform flag takes one of three values:
o 2 - The image is encoded as YCCK (implicitly converted from CMYK on encoding).
o 1 - The image is encoded as YCbCr (implicitly converted from RGB on encoding).
o 0 - Unknown. 3-channel images are assumed to be RGB, 4-channel images are assumed to be CMYK.
If neither marker segment is present, the following procedure is followed: Single-channel images are assumed
to be grayscale, and 2-channel images are assumed to be grayscale with an alpha channel. For 3- and 4-channel
images, the component ids are consulted. If these values are 1-3 for a 3-channel image, then the image is
assumed to be YCbCr. If these values are 1-4 for a 4-channel image, then the image is assumed to be YCbCrA. If
these values are > 4, they are checked against the ASCII codes for 'R', 'G', 'B', 'A', 'C', 'c', 'M', 'Y', 'K'.
These can encode the following colorspaces:
RGB
RGBA
YCC (as 'Y','C','c'), assumed to be PhotoYCC
YCCA (as 'Y','C','c','A'), assumed to be PhotoYCCA
CMYK (as 'C', 'M', 'Y', 'K').
Otherwise, 3-channel subsampled images are assumed to be YCbCr, 3-channel non-subsampled images are assumed to
be RGB, 4-channel subsampled images are assumed to be YCCK, and 4-channel, non-subsampled images are assumed to
be CMYK.
All other images are declared uninterpretable and an exception is thrown if an attempt is made to read one as
a BufferedImage. Such an image may be read only as a Raster. If an image is interpretable but there is no Java
ColorSpace available corresponding to the encoded colorspace (e.g. YCbCr/YCCK), then ImageReader.getRawImageType
will return null.
*/
if (adobeDCT != null) {
switch (adobeDCT.getTransform()) {
case AdobeDCT.YCC:
return JPEGColorSpace.YCbCr;
case AdobeDCT.YCCK:
return JPEGColorSpace.YCCK;
case AdobeDCT.Unknown:
if (startOfFrame.components.length == 1) {
return JPEGColorSpace.Gray;
}
else if (startOfFrame.components.length == 3) {
return JPEGColorSpace.RGB;
}
else if (startOfFrame.components.length == 4) {
return JPEGColorSpace.CMYK;
}
// Else fall through
default:
}
}
switch (startOfFrame.components.length) {
case 1:
return JPEGColorSpace.Gray;
case 2:
return JPEGColorSpace.GrayA;
case 3:
if (startOfFrame.components[0].id == 1 && startOfFrame.components[1].id == 2 && startOfFrame.components[2].id == 3) {
return JPEGColorSpace.YCbCr;
}
else if (startOfFrame.components[0].id == 'R' && startOfFrame.components[1].id == 'G' && startOfFrame.components[2].id == 'B') {
return JPEGColorSpace.RGB;
}
else if (startOfFrame.components[0].id == 'Y' && startOfFrame.components[1].id == 'C' && startOfFrame.components[2].id == 'c') {
return JPEGColorSpace.PhotoYCC;
}
else {
// if subsampled, YCbCr else RGB
for (SOFComponent component : startOfFrame.components) {
if (component.hSub != 1 || component.vSub != 1) {
return JPEGColorSpace.YCbCr;
}
}
return JPEGColorSpace.RGB;
}
case 4:
if (startOfFrame.components[0].id == 1 && startOfFrame.components[1].id == 2 && startOfFrame.components[2].id == 3 && startOfFrame.components[3].id == 4) {
return JPEGColorSpace.YCbCrA;
}
else if (startOfFrame.components[0].id == 'R' && startOfFrame.components[1].id == 'G' && startOfFrame.components[2].id == 'B' && startOfFrame.components[3].id == 'A') {
return JPEGColorSpace.RGBA;
}
else if (startOfFrame.components[0].id == 'Y' && startOfFrame.components[1].id == 'C' && startOfFrame.components[2].id == 'c' && startOfFrame.components[3].id == 'A') {
return JPEGColorSpace.PhotoYCCA;
}
else if (startOfFrame.components[0].id == 'C' && startOfFrame.components[1].id == 'M' && startOfFrame.components[2].id == 'Y' && startOfFrame.components[3].id == 'K') {
return JPEGColorSpace.CMYK;
}
else if (startOfFrame.components[0].id == 'Y' && startOfFrame.components[1].id == 'C' && startOfFrame.components[2].id == 'c' && startOfFrame.components[3].id == 'K') {
return JPEGColorSpace.YCCK;
}
else {
// if subsampled, YCCK else CMYK
for (SOFComponent component : startOfFrame.components) {
if (component.hSub != 1 || component.vSub != 1) {
return JPEGColorSpace.YCCK;
}
}
return JPEGColorSpace.CMYK;
}
default:
throw new IIOException("Cannot determine source color space");
}
}
private ICC_Profile ensureDisplayProfile(final ICC_Profile profile) {
// 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
@ -608,20 +689,7 @@ public class JPEGImageReader extends ImageReaderBase {
if (!jfif.isEmpty()) {
JPEGSegment segment = jfif.get(0);
DataInputStream stream = new DataInputStream(segment.data());
int x, y;
return new JFIFSegment(
stream.readUnsignedByte(),
stream.readUnsignedByte(),
stream.readUnsignedByte(),
stream.readUnsignedShort(),
stream.readUnsignedShort(),
x = stream.readUnsignedByte(),
y = stream.readUnsignedByte(),
readFully(stream, x * y * 3)
);
return JFIFSegment.read(segment.data());
}
return null;
@ -632,13 +700,7 @@ public class JPEGImageReader extends ImageReaderBase {
if (!jfxx.isEmpty()) {
JPEGSegment segment = jfxx.get(0);
DataInputStream stream = new DataInputStream(segment.data());
return new JFXXSegment(
stream.readUnsignedByte(),
readFully(stream, segment.length() - 1)
);
return JFXXSegment.read(segment.data(), segment.length());
}
return null;
@ -669,10 +731,10 @@ public class JPEGImageReader extends ImageReaderBase {
int chunkCount = stream.readUnsignedByte();
if (chunkNumber != 1 && chunkCount != 1) {
throw new IIOException(String.format("Bad number of 'ICC_PROFILE' chunks."));
throw new IIOException(String.format("Bad number of 'ICC_PROFILE' chunks: %d of %d.", chunkNumber, chunkCount));
}
return ICC_Profile.getInstance(stream);
return readICCProfileSafe(stream);
}
else if (!segments.isEmpty()) {
// NOTE: This is probably over-complicated, as I've never encountered ICC_PROFILE chunks out of order...
@ -680,17 +742,18 @@ public class JPEGImageReader extends ImageReaderBase {
int chunkNumber = stream.readUnsignedByte();
int chunkCount = stream.readUnsignedByte();
// 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.");
// Some weird JPEGs use 0-based indexes... count == 0 and all numbers == 0. Ignore these profiles
processWarningOccurred("Invalid 'ICC_PROFILE' chunk index: " + chunkNumber + ". Ignoring ICC profile.");
return null;
}
boolean badICC = false;
if (chunkCount != segments.size()) {
// Others use count == 1, and all numbers == 1.
// Handle these by issuing warning
badICC = true;
processWarningOccurred("Unexpected ICC profile chunk count: " + chunkCount + ". Ignoring count, assuming " + segments.size() + " chunks in sequence.");
processWarningOccurred("Unexpected 'ICC_PROFILE' chunk count: " + chunkCount + ". Ignoring count, assuming " + segments.size() + " chunks in sequence.");
}
int count = badICC ? segments.size() : chunkCount;
@ -703,18 +766,30 @@ public class JPEGImageReader extends ImageReaderBase {
chunkNumber = stream.readUnsignedByte();
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: %d of %d.", chunkNumber, chunkCount));
}
streams[badICC ? i : chunkNumber - 1] = stream;
}
return ICC_Profile.getInstance(new SequenceInputStream(Collections.enumeration(Arrays.asList(streams))));
return readICCProfileSafe(new SequenceInputStream(Collections.enumeration(Arrays.asList(streams))));
}
return null;
}
private ICC_Profile readICCProfileSafe(final InputStream stream) throws IOException {
try {
return ICC_Profile.getInstance(stream);
}
catch (RuntimeException e) {
// NOTE: Throws either IllegalArgumentException or CMMException, depending on platform.
// Usual reason: Broken tools store truncated ICC profiles in a single ICC_PROFILE chunk...
processWarningOccurred(String.format("Bad 'ICC_PROFILE' chunk(s): %s. Ignoring ICC profile.", e.getMessage()));
return null;
}
}
@Override
public boolean canReadRaster() {
return delegate.canReadRaster();
@ -739,7 +814,7 @@ public class JPEGImageReader extends ImageReaderBase {
@Override
public boolean readerSupportsThumbnails() {
return true; // We support EXIF, JFIF and JFXX style thumbnails, if present
return true; // We support EXIF, JFIF and JFXX style thumbnails
}
private void readThumbnailMetadata(int imageIndex) throws IOException {
@ -747,47 +822,54 @@ public class JPEGImageReader extends ImageReaderBase {
if (thumbnails == null) {
thumbnails = new ArrayList<ThumbnailReader>();
ThumbnailReadProgressListener thumbnailProgressDelegator = new ThumbnailProgressDelegate();
// Read JFIF thumbnails if present
JFIFSegment jfif = getJFIF();
if (jfif != null && jfif.thumbnail != null) {
thumbnails.add(new JFIFThumbnailReader(this, imageIndex, thumbnails.size(), jfif));
thumbnails.add(new JFIFThumbnailReader(thumbnailProgressDelegator, imageIndex, thumbnails.size(), jfif));
}
// Read JFXX thumbnails if present
JFXXSegment jfxx = getJFXX();
if (jfxx != null && jfxx.thumbnail != null) {
switch (jfxx.extensionCode) {
case JFXXSegment.JPEG:
case JFXXSegment.INDEXED:
case JFXXSegment.RGB:
thumbnails.add(new JFXXThumbnailReader(this, imageIndex, thumbnails.size(), jfxx));
thumbnails.add(new JFXXThumbnailReader(thumbnailProgressDelegator, imageIndex, thumbnails.size(), jfxx));
break;
default:
processWarningOccurred("Unknown JFXX extension code: " + jfxx.extensionCode);
}
}
// Read Exif thumbnails if present
List<JPEGSegment> exifSegments = getAppSegments(JPEG.APP1, "Exif");
if (!exifSegments.isEmpty()) {
JPEGSegment exif = exifSegments.get(0);
InputStream data = exif.data();
//noinspection ResultOfMethodCallIgnored
data.read(); // Pad
ImageInputStream stream = ImageIO.createImageInputStream(data);
CompoundDirectory exifMetadata = (CompoundDirectory) new EXIFReader().read(stream);
if (data.read() == -1) {
// Pad
processWarningOccurred("Exif chunk has no data.");
}
else {
ImageInputStream stream = ImageIO.createImageInputStream(data);
CompoundDirectory exifMetadata = (CompoundDirectory) new EXIFReader().read(stream);
if (exifMetadata.directoryCount() == 2) {
Directory ifd1 = exifMetadata.getDirectory(1);
if (exifMetadata.directoryCount() == 2) {
Directory ifd1 = exifMetadata.getDirectory(1);
Entry compression = ifd1.getEntryById(TIFF.TAG_COMPRESSION);
Entry compression = ifd1.getEntryById(TIFF.TAG_COMPRESSION);
// 1 = no compression, 6 = JPEG compression (default)
if (compression == null || compression.getValue().equals(1) || compression.getValue().equals(6)) {
thumbnails.add(new EXIFThumbnailReader(this, 0, thumbnails.size(), ifd1, stream));
}
else {
processWarningOccurred("EXIF IFD with unknown compression: " + compression.getValue());
// 1 = no compression, 6 = JPEG compression (default)
if (compression == null || compression.getValue().equals(1) || compression.getValue().equals(6)) {
thumbnails.add(new EXIFThumbnailReader(thumbnailProgressDelegator, 0, thumbnails.size(), ifd1, stream));
}
else {
processWarningOccurred("EXIF IFD with unknown compression (expected 1 or 6): " + compression.getValue());
}
}
}
}
@ -827,26 +909,6 @@ public class JPEGImageReader extends ImageReaderBase {
return thumbnails.get(thumbnailIndex).read();
}
@Override
protected void processWarningOccurred(String warning) {
super.processWarningOccurred(warning);
}
@Override
protected void processThumbnailStarted(int imageIndex, int thumbnailIndex) {
super.processThumbnailStarted(imageIndex, thumbnailIndex);
}
@Override
protected void processThumbnailProgress(float percentageDone) {
super.processThumbnailProgress(percentageDone);
}
@Override
protected void processThumbnailComplete() {
super.processThumbnailComplete();
}
private static void invertCMYK(final Raster raster) {
byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
@ -1058,6 +1120,20 @@ public class JPEGImageReader extends ImageReaderBase {
}
}
private class ThumbnailProgressDelegate implements ThumbnailReadProgressListener {
public void processThumbnailStarted(int imageIndex, int thumbnailIndex) {
JPEGImageReader.this.processThumbnailStarted(imageIndex, thumbnailIndex);
}
public void processThumbnailProgress(float percentageDone) {
JPEGImageReader.this.processThumbnailProgress(percentageDone);
}
public void processThumbnailComplete() {
JPEGImageReader.this.processThumbnailComplete();
}
}
private static class SOF {
private final int marker;
private final int samplePrecision;

View File

@ -0,0 +1,44 @@
/*
* 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;
/**
* ThumbnailReadProgressListener
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: ThumbnailReadProgressListener.java,v 1.0 07.05.12 10:15 haraldk Exp$
*/
interface ThumbnailReadProgressListener {
void processThumbnailStarted(int imageIndex, int thumbnailIndex);
void processThumbnailProgress(float percentageDone);
void processThumbnailComplete();
}

View File

@ -44,25 +44,25 @@ import java.io.InputStream;
*/
abstract class ThumbnailReader {
private final JPEGImageReader parent;
private final ThumbnailReadProgressListener progressListener;
protected final int imageIndex;
protected final int thumbnailIndex;
protected ThumbnailReader(final JPEGImageReader parent, final int imageIndex, final int thumbnailIndex) {
this.parent = parent;
protected ThumbnailReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex) {
this.progressListener = progressListener;
this.imageIndex = imageIndex;
this.thumbnailIndex = thumbnailIndex;
}
protected final void processThumbnailStarted() {
parent.processThumbnailStarted(imageIndex, thumbnailIndex);
progressListener.processThumbnailStarted(imageIndex, thumbnailIndex);
}
protected final void processThumbnailProgress(float percentageDone) {
parent.processThumbnailProgress(percentageDone);
progressListener.processThumbnailProgress(percentageDone);
}
protected final void processThumbnailComplete() {
parent.processThumbnailComplete();
progressListener.processThumbnailComplete();
}
static protected BufferedImage readJPEGThumbnail(InputStream stream) throws IOException {

View File

@ -0,0 +1,63 @@
/*
* 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.stream.URLImageInputStreamSpi;
import javax.imageio.ImageIO;
import javax.imageio.spi.IIORegistry;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.net.URL;
import static org.junit.Assert.assertNotNull;
/**
* AbstractThumbnailReaderTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: AbstractThumbnailReaderTest.java,v 1.0 04.05.12 15:55 haraldk Exp$
*/
public abstract class AbstractThumbnailReaderTest {
static {
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
}
protected abstract ThumbnailReader createReader(
ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, ImageInputStream stream
) throws IOException;
protected final ImageInputStream createStream(final String name) throws IOException {
URL resource = getClass().getResource(name);
ImageInputStream stream = ImageIO.createImageInputStream(resource);
assertNotNull("Could not create stream for resource " + resource, stream);
return stream;
}
}

View File

@ -0,0 +1,129 @@
/*
* 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.CompoundDirectory;
import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
import org.junit.Test;
import org.mockito.InOrder;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* EXIFThumbnailReaderTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: EXIFThumbnailReaderTest.java,v 1.0 04.05.12 15:55 haraldk Exp$
*/
public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
@Override
protected EXIFThumbnailReader createReader(final ThumbnailReadProgressListener progressListener, final int imageIndex, final int thumbnailIndex, final ImageInputStream stream) throws IOException {
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP1, "Exif");
stream.close();
assertNotNull(segments);
assertFalse(segments.isEmpty());
EXIFReader reader = new EXIFReader();
InputStream data = segments.get(0).data();
if (data.read() < 0) {
throw new AssertionError("EOF!");
}
ImageInputStream exifStream = ImageIO.createImageInputStream(data);
CompoundDirectory ifds = (CompoundDirectory) reader.read(exifStream);
assertEquals(2, ifds.directoryCount());
return new EXIFThumbnailReader(progressListener, imageIndex, thumbnailIndex, ifds.getDirectory(1), exifStream);
}
@Test
public void testReadJPEG() throws IOException {
ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/cmyk-sample-multiple-chunk-icc.jpg"));
assertEquals(114, reader.getWidth());
assertEquals(160, reader.getHeight());
BufferedImage thumbnail = reader.read();
assertNotNull(thumbnail);
assertEquals(114, thumbnail.getWidth());
assertEquals(160, thumbnail.getHeight());
}
@Test
public void testReadRaw() throws IOException {
ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/exif-rgb-thumbnail-sony-d700.jpg"));
assertEquals(80, reader.getWidth());
assertEquals(60, reader.getHeight());
BufferedImage thumbnail = reader.read();
assertNotNull(thumbnail);
assertEquals(80, thumbnail.getWidth());
assertEquals(60, thumbnail.getHeight());
}
@Test
public void testProgressListenerJPEG() throws IOException {
ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class);
createReader(listener, 42, 43, createStream("/jpeg/cmyk-sample-multiple-chunk-icc.jpg")).read();
InOrder order = inOrder(listener);
order.verify(listener).processThumbnailStarted(42, 43);
order.verify(listener, atLeastOnce()).processThumbnailProgress(100f);
order.verify(listener).processThumbnailComplete();
}
@Test
public void testProgressListenerRaw() throws IOException {
ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class);
createReader(listener, 0, 99, createStream("/jpeg/exif-rgb-thumbnail-sony-d700.jpg")).read();
InOrder order = inOrder(listener);
order.verify(listener).processThumbnailStarted(0, 99);
order.verify(listener, atLeastOnce()).processThumbnailProgress(100f);
order.verify(listener).processThumbnailComplete();
}
}

View File

@ -0,0 +1,90 @@
/*
* 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 org.junit.Test;
import org.mockito.InOrder;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.List;
import static org.junit.Assert.*;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
/**
* JFIFThumbnailReaderTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: JFIFThumbnailReaderTest.java,v 1.0 04.05.12 15:56 haraldk Exp$
*/
public class JFIFThumbnailReaderTest extends AbstractThumbnailReaderTest {
@Override
protected JFIFThumbnailReader createReader(ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, ImageInputStream stream) throws IOException {
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP0, "JFIF");
stream.close();
assertNotNull(segments);
assertFalse(segments.isEmpty());
return new JFIFThumbnailReader(progressListener, imageIndex, thumbnailIndex, JFIFSegment.read(segments.get(0).data()));
}
@Test
public void testReadRaw() throws IOException {
ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/jfif-jfif-and-exif-thumbnail-sharpshot-iphone.jpg"));
assertEquals(131, reader.getWidth());
assertEquals(122, reader.getHeight());
BufferedImage thumbnail = reader.read();
assertNotNull(thumbnail);
assertEquals(131, thumbnail.getWidth());
assertEquals(122, thumbnail.getHeight());
}
@Test
public void testProgressListenerRaw() throws IOException {
ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class);
createReader(listener, 0, 99, createStream("/jpeg/jfif-jfif-and-exif-thumbnail-sharpshot-iphone.jpg")).read();
InOrder order = inOrder(listener);
order.verify(listener).processThumbnailStarted(0, 99);
order.verify(listener, atLeastOnce()).processThumbnailProgress(100f);
order.verify(listener).processThumbnailComplete();
}
}

View File

@ -0,0 +1,96 @@
/*
* 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 org.junit.Test;
import org.mockito.InOrder;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
/**
* JFXXThumbnailReaderTest
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: JFXXThumbnailReaderTest.java,v 1.0 04.05.12 15:56 haraldk Exp$
*/
public class JFXXThumbnailReaderTest extends AbstractThumbnailReaderTest {
@Override
protected JFXXThumbnailReader createReader(ThumbnailReadProgressListener progressListener, int imageIndex, int thumbnailIndex, ImageInputStream stream) throws IOException {
List<JPEGSegment> segments = JPEGSegmentUtil.readSegments(stream, JPEG.APP0, "JFXX");
stream.close();
assertNotNull(segments);
assertFalse(segments.isEmpty());
JPEGSegment jfxx = segments.get(0);
return new JFXXThumbnailReader(progressListener, imageIndex, thumbnailIndex, JFXXSegment.read(jfxx.data(), jfxx.length()));
}
@Test
public void testReadJPEG() throws IOException {
ThumbnailReader reader = createReader(mock(ThumbnailReadProgressListener.class), 0, 0, createStream("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg"));
assertEquals(80, reader.getWidth());
assertEquals(60, reader.getHeight());
BufferedImage thumbnail = reader.read();
assertNotNull(thumbnail);
assertEquals(80, thumbnail.getWidth());
assertEquals(60, thumbnail.getHeight());
}
// TODO: Test JFXX indexed thumbnail
// TODO: Test JFXX RGB thumbnail
@Test
public void testProgressListenerRaw() throws IOException {
ThumbnailReadProgressListener listener = mock(ThumbnailReadProgressListener.class);
createReader(listener, 0, 99, createStream("/jpeg/jfif-jfxx-thumbnail-olympus-d320l.jpg")).read();
InOrder order = inOrder(listener);
order.verify(listener).processThumbnailStarted(0, 99);
order.verify(listener, atLeastOnce()).processThumbnailProgress(100f);
order.verify(listener).processThumbnailComplete();
}
}

View File

@ -33,17 +33,19 @@ import org.junit.Test;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageReaderSpi;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
/**
* JPEGImageReaderTest
@ -138,6 +140,8 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
assertEquals(expectedData.length, data.length);
assertJPEGPixelsEqual(expectedData, data, 0);
reader.dispose();
}
private static void assertJPEGPixelsEqual(byte[] expected, byte[] actual, int actualOffset) {
@ -160,6 +164,8 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
assertNotNull(image);
assertEquals(345, image.getWidth());
assertEquals(540, image.getHeight());
reader.dispose();
}
@Test
@ -175,10 +181,33 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
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());
reader.dispose();
}
@Test
public void testTruncatedICCProfile() throws IOException {
// File contains single 'ICC_PROFILE' chunk, with a truncated (32 000 bytes) "Europe ISO Coated FOGRA27" ICC profile (by Adobe).
// Profile should have been about 550 000 bytes, split into multiple chunks. Written by GIMP 2.6.11
// See: https://bugzilla.redhat.com/show_bug.cgi?id=695246
JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/cmm-exception-invalid-icc-profile-data.jpg")));
assertEquals(1993, reader.getWidth(0));
assertEquals(1038, reader.getHeight(0));
ImageReadParam param = reader.getDefaultReadParam();
param.setSourceRegion(new Rectangle(reader.getWidth(0), 8));
BufferedImage image = reader.read(0, param);
assertNotNull(image);
assertEquals(1993, image.getWidth());
assertEquals(8, image.getHeight());
reader.dispose();
}
@Test
@ -198,6 +227,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
assertEquals(449, image.getHeight());
// TODO: Need to test colors!
reader.dispose();
}
@Test
@ -414,4 +444,112 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5);
}
}
private List<TestData> getCMYKData() {
return Arrays.asList(
new TestData(getClassLoaderResource("/jpeg/cmyk-sample.jpg"), new Dimension(100, 100)),
new TestData(getClassLoaderResource("/jpeg/cmyk-sample-multiple-chunk-icc.jpg"), new Dimension(100, 100)),
new TestData(getClassLoaderResource("/jpeg/cmyk-sample-custom-icc-bright.jpg"), new Dimension(100, 100)),
new TestData(getClassLoaderResource("/jpeg/cmyk-sample-no-icc.jpg"), new Dimension(100, 100))
);
}
@Test
public void testGetImageTypesCMYK() throws IOException {
// Make sure CMYK images will report their embedded color profile among image types
JPEGImageReader reader = createReader();
List<TestData> cmykData = getCMYKData();
for (TestData data : cmykData) {
reader.setInput(data.getInputStream());
Iterator<ImageTypeSpecifier> types = reader.getImageTypes(0);
assertTrue(data + " has no image types", types.hasNext());
boolean hasRGBType = false;
boolean hasCMYKType = false;
while (types.hasNext()) {
ImageTypeSpecifier type = types.next();
int csType = type.getColorModel().getColorSpace().getType();
if (csType == ColorSpace.TYPE_RGB) {
hasRGBType = true;
}
else if (csType == ColorSpace.TYPE_CMYK) {
assertTrue("CMYK types should be delivered after RGB types (violates \"contract\" of more \"natural\" type first) for " + data, hasRGBType);
hasCMYKType = true;
break;
}
}
assertTrue("No RGB types for " + data, hasRGBType);
assertTrue("No CMYK types for " + data, hasCMYKType);
}
reader.dispose();
}
@Test
public void testGetRawImageTypeCMYK() throws IOException {
// Make sure images that are encoded as CMYK (not YCCK) actually return non-null for getRawImageType
JPEGImageReader reader = createReader();
List<TestData> cmykData = Arrays.asList(
new TestData(getClassLoaderResource("/jpeg/cmyk-sample.jpg"), new Dimension(100, 100)),
new TestData(getClassLoaderResource("/jpeg/cmyk-sample-no-icc.jpg"), new Dimension(100, 100))
);
for (TestData data : cmykData) {
reader.setInput(data.getInputStream());
ImageTypeSpecifier rawType = reader.getRawImageType(0);
assertNotNull("No raw type for " + data, rawType);
}
}
@Test
public void testReadCMYKAsCMYK() throws IOException {
// Make sure CMYK images can be read and still contain their original (embedded) color profile
JPEGImageReader reader = createReader();
List<TestData> cmykData = getCMYKData();
for (TestData data : cmykData) {
reader.setInput(data.getInputStream());
Iterator<ImageTypeSpecifier> types = reader.getImageTypes(0);
assertTrue(data + " has no image types", types.hasNext());
ImageTypeSpecifier cmykType = null;
while (types.hasNext()) {
ImageTypeSpecifier type = types.next();
int csType = type.getColorModel().getColorSpace().getType();
if (csType == ColorSpace.TYPE_CMYK) {
cmykType = type;
break;
}
}
assertNotNull("No CMYK types for " + data, cmykType);
ImageReadParam param = reader.getDefaultReadParam();
param.setDestinationType(cmykType);
param.setSourceRegion(new Rectangle(reader.getWidth(0), 8)); // We don't really need to read it all
BufferedImage image = reader.read(0, param);
assertNotNull(image);
assertEquals(ColorSpace.TYPE_CMYK, image.getColorModel().getColorSpace().getType());
}
reader.dispose();
}
// TODO: Test RGBA/YCbCrA handling
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB