mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-02 11:05:29 -04:00
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:
parent
aaef2e4fad
commit
a4dfb7a009
@ -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;
|
||||
|
@ -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 = {
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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 |
Loading…
x
Reference in New Issue
Block a user