diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/AdobeDCT.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/AdobeDCT.java index 9c2be75a..c95a8355 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/AdobeDCT.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/AdobeDCT.java @@ -29,12 +29,12 @@ package com.twelvemonkeys.imageio.plugins.jpeg; /** -* AdobeDCT -* -* @author Harald Kuhr -* @author last modified by $Author: haraldk$ -* @version $Id: AdobeDCT.java,v 1.0 23.04.12 16:55 haraldk Exp$ -*/ + * AdobeDCT + * + * @author Harald Kuhr + * @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; diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java index bf6e9093..d18dbdf6 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java @@ -57,8 +57,8 @@ final class EXIFThumbnailReader extends ThumbnailReader { private transient SoftReference 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 = { diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFSegment.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFSegment.java index 71497a3f..373e1476 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFSegment.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFSegment.java @@ -28,13 +28,17 @@ package com.twelvemonkeys.imageio.plugins.jpeg; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; + /** -* JFIFSegment -* -* @author Harald Kuhr -* @author last modified by $Author: haraldk$ -* @version $Id: JFIFSegment.java,v 1.0 23.04.12 16:52 haraldk Exp$ -*/ + * JFIFSegment + * + * @author Harald Kuhr + * @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) + ); + } } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReader.java index a696dc27..71e25bb4 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReader.java @@ -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(); diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXSegment.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXSegment.java index edb03f66..00d33534 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXSegment.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXSegment.java @@ -28,13 +28,17 @@ package com.twelvemonkeys.imageio.plugins.jpeg; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; + /** -* JFXXSegment -* -* @author Harald Kuhr -* @author last modified by $Author: haraldk$ -* @version $Id: JFXXSegment.java,v 1.0 23.04.12 16:54 haraldk Exp$ -*/ + * JFXXSegment + * + * @author Harald Kuhr + * @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) + ); + } } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java index c70a8058..cce8b3bb 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReader.java @@ -49,8 +49,8 @@ final class JFXXThumbnailReader extends ThumbnailReader { private transient SoftReference 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; } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGColorSpace.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGColorSpace.java new file mode 100644 index 00000000..157648fc --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGColorSpace.java @@ -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 Harald Kuhr + * @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 +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 21e587fd..eff81ca0 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -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 Harald Kuhr * @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 getImageTypes(int imageIndex) throws IOException { - // TODO: Read header, and make sure we return valid types for the images we can now read - Iterator 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 typeList = new ArrayList(); + // 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 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 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(); + 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 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; diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReadProgressListener.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReadProgressListener.java new file mode 100644 index 00000000..b6b201ed --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReadProgressListener.java @@ -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 Harald Kuhr + * @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(); +} diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java index 48831bc5..6354ef93 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/ThumbnailReader.java @@ -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 { diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/AbstractThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/AbstractThumbnailReaderTest.java new file mode 100644 index 00000000..9cd65353 --- /dev/null +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/AbstractThumbnailReaderTest.java @@ -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 Harald Kuhr + * @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; + } +} diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java new file mode 100644 index 00000000..f1b92e27 --- /dev/null +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java @@ -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 Harald Kuhr + * @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 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(); + } +} diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReaderTest.java new file mode 100644 index 00000000..77b69653 --- /dev/null +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFIFThumbnailReaderTest.java @@ -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 Harald Kuhr + * @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 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(); + } +} diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java new file mode 100644 index 00000000..4f8db3ff --- /dev/null +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JFXXThumbnailReaderTest.java @@ -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 Harald Kuhr + * @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 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(); + } +} diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java index 37f64ee4..adf3629d 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java @@ -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 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 cmykData = getCMYKData(); + + for (TestData data : cmykData) { + reader.setInput(data.getInputStream()); + Iterator 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 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 cmykData = getCMYKData(); + + for (TestData data : cmykData) { + reader.setInput(data.getInputStream()); + Iterator 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 } diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-invalid-icc-profile-data.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-invalid-icc-profile-data.jpg new file mode 100644 index 00000000..a34fd4ee Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/cmm-exception-invalid-icc-profile-data.jpg differ diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample-no-icc.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample-no-icc.jpg new file mode 100644 index 00000000..3e4e7587 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/cmyk-sample-no-icc.jpg differ