#573: Always return RAWImageType for JPEG.

+ Bonus: Fix luma to gray conversion
This commit is contained in:
Harald Kuhr 2021-04-10 11:44:09 +02:00
parent b67975eef7
commit 419ffc9373
7 changed files with 340 additions and 126 deletions

View File

@ -97,10 +97,6 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
protected abstract List<String> getMIMETypes(); protected abstract List<String> getMIMETypes();
protected boolean allowsNullRawImageType() {
return false;
}
protected static void failBecause(String message, Throwable exception) { protected static void failBecause(String message, Throwable exception) {
throw new AssertionError(message, exception); throw new AssertionError(message, exception);
} }
@ -221,6 +217,7 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
image = reader.read(i); image = reader.read(i);
} }
catch (Exception e) { catch (Exception e) {
e.printStackTrace();
failBecause(String.format("Image %s index %s could not be read: %s", data.getInput(), i, e), e); failBecause(String.format("Image %s index %s could not be read: %s", data.getInput(), i, e), e);
} }
@ -1359,9 +1356,6 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
reader.setInput(data.getInputStream()); reader.setInput(data.getInputStream());
ImageTypeSpecifier rawType = reader.getRawImageType(0); ImageTypeSpecifier rawType = reader.getRawImageType(0);
if (rawType == null && allowsNullRawImageType()) {
continue;
}
assertNotNull(rawType); assertNotNull(rawType);
Iterator<ImageTypeSpecifier> types = reader.getImageTypes(0); Iterator<ImageTypeSpecifier> types = reader.getImageTypes(0);
@ -1383,6 +1377,7 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
assertTrue("ImageTypeSpecifier from getRawImageType should be in the iterator from getImageTypes", rawFound); assertTrue("ImageTypeSpecifier from getRawImageType should be in the iterator from getImageTypes", rawFound);
} }
reader.dispose(); reader.dispose();
} }

View File

@ -62,6 +62,7 @@ class FastCMYKToRGB implements /*BufferedImageOp,*/ RasterOp {
* @return {@code dest}, or a new {@link WritableRaster} if {@code dest} is {@code null}. * @return {@code dest}, or a new {@link WritableRaster} if {@code dest} is {@code null}.
* @throws IllegalArgumentException if {@code src} and {@code dest} refer to the same object * @throws IllegalArgumentException if {@code src} and {@code dest} refer to the same object
*/ */
@Override
public WritableRaster filter(Raster src, WritableRaster dest) { public WritableRaster filter(Raster src, WritableRaster dest) {
Validate.notNull(src, "src may not be null"); Validate.notNull(src, "src may not be null");
// TODO: Why not allow same raster, if converting to 4 byte ABGR? // TODO: Why not allow same raster, if converting to 4 byte ABGR?
@ -142,10 +143,12 @@ class FastCMYKToRGB implements /*BufferedImageOp,*/ RasterOp {
rgb[2] = (byte) (255 - (((cmyk[2] & 0xFF) * (255 - k) / 255) + k)); rgb[2] = (byte) (255 - (((cmyk[2] & 0xFF) * (255 - k) / 255) + k));
} }
@Override
public Rectangle2D getBounds2D(Raster src) { public Rectangle2D getBounds2D(Raster src) {
return src.getBounds(); return src.getBounds();
} }
@Override
public WritableRaster createCompatibleDestRaster(final Raster src) { public WritableRaster createCompatibleDestRaster(final Raster src) {
// WHAT?? This code no longer work for JRE 7u45+... JRE bug?! // WHAT?? This code no longer work for JRE 7u45+... JRE bug?!
// Raster child = src.createChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, new int[] {0, 1, 2}); // Raster child = src.createChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, new int[] {0, 1, 2});
@ -157,6 +160,7 @@ class FastCMYKToRGB implements /*BufferedImageOp,*/ RasterOp {
return raster.createWritableChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, new int[] {0, 1, 2}); return raster.createWritableChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, new int[] {0, 1, 2});
} }
@Override
public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
if (dstPt == null) { if (dstPt == null) {
dstPt = new Point2D.Double(srcPt.getX(), srcPt.getY()); dstPt = new Point2D.Double(srcPt.getX(), srcPt.getY());
@ -168,6 +172,7 @@ class FastCMYKToRGB implements /*BufferedImageOp,*/ RasterOp {
return dstPt; return dstPt;
} }
@Override
public RenderingHints getRenderingHints() { public RenderingHints getRenderingHints() {
return null; return null;
} }

View File

@ -194,73 +194,38 @@ public final class JPEGImageReader extends ImageReaderBase {
@Override @Override
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException { public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
checkBounds(imageIndex); ImageTypeSpecifier rawImageType = getRawImageType(imageIndex);
initHeader(imageIndex); ColorModel rawColorModel = rawImageType.getColorModel();
JPEGColorSpace sourceCSType = getSourceCSType(getJFIF(), getAdobeDCT(), getSOF());
Iterator<ImageTypeSpecifier> types; Set<ImageTypeSpecifier> types = new LinkedHashSet<>();
try {
types = delegate.getImageTypes(0);
}
catch (IndexOutOfBoundsException | NegativeArraySizeException ignore) {
types = null;
}
JPEGColorSpace csType = getSourceCSType(getJFIF(), getAdobeDCT(), getSOF()); if (rawColorModel.getColorSpace().getType() != ColorSpace.TYPE_GRAY) {
// Add the standard types, we can always convert to these, except for gray
if (types == null || !types.hasNext() || csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK) { if (rawColorModel.hasAlpha()) {
ArrayList<ImageTypeSpecifier> typeList = new ArrayList<>(); types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB));
// Add the standard types, we can always convert to these types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR));
typeList.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR)); types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB_PRE));
typeList.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB)); types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR_PRE));
typeList.add(ImageTypeSpecifiers.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(false);
if (csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK) {
if (profile != null && profile.getNumComponents() == 4) {
typeList.add(ImageTypeSpecifiers.createInterleaved(ColorSpaces.createColorSpace(profile), new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false));
}
typeList.add(ImageTypeSpecifiers.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 && profile.getNumComponents() == 3) {
typeList.add(ImageTypeSpecifiers.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(
ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB),
ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR),
ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB_PRE),
ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR_PRE)
));
if (profile != null && profile.getNumComponents() == 3) {
typeList.add(ImageTypeSpecifiers.createInterleaved(ColorSpaces.createColorSpace(profile), new int[] {0, 1, 2, 3}, DataBuffer.TYPE_BYTE, true, false));
}
} }
return typeList.iterator(); types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR));
types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB));
types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR));
} }
else if (csType == JPEGColorSpace.RGB) {
// Bug in com.sun...JPEGImageReader: returns gray as acceptable type, but refuses to convert
ArrayList<ImageTypeSpecifier> typeList = new ArrayList<>();
// Filter out the gray type types.add(rawImageType);
while (types.hasNext()) {
ImageTypeSpecifier type = types.next(); // If the source type has a luminance (Y) component, we can also convert to gray
if (type.getBufferedImageType() != BufferedImage.TYPE_BYTE_GRAY) { if (sourceCSType != JPEGColorSpace.RGB && sourceCSType != JPEGColorSpace.RGBA && sourceCSType != JPEGColorSpace.CMYK) {
typeList.add(type); if (rawColorModel.hasAlpha()) {
} types.add(ImageTypeSpecifiers.createGrayscale(8, DataBuffer.TYPE_BYTE, false));
} }
return typeList.iterator(); types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY));
} }
return types; return types.iterator();
} }
@Override @Override
@ -268,34 +233,55 @@ public final class JPEGImageReader extends ImageReaderBase {
checkBounds(imageIndex); checkBounds(imageIndex);
initHeader(imageIndex); initHeader(imageIndex);
// If delegate can determine the spec, we'll just go with that // Consult the image metadata
try {
ImageTypeSpecifier rawType = delegate.getRawImageType(0);
if (rawType != null) {
return rawType;
}
}
catch (IIOException | NullPointerException | ArrayIndexOutOfBoundsException | NegativeArraySizeException ignore) {
// Fall through
}
// Otherwise, consult the image metadata
JPEGColorSpace csType = getSourceCSType(getJFIF(), getAdobeDCT(), getSOF()); JPEGColorSpace csType = getSourceCSType(getJFIF(), getAdobeDCT(), getSOF());
ICC_Profile profile = getEmbeddedICCProfile(false);
ColorSpace cs;
boolean hasAlpha = false;
switch (csType) { switch (csType) {
case CMYK: case GrayA:
// Create based on embedded profile if exists, or create from "Generic CMYK" hasAlpha = true;
ICC_Profile profile = getEmbeddedICCProfile(false); case Gray:
// Create based on embedded profile if exists, otherwise create from Gray
cs = profile != null && profile.getNumComponents() == 1
? ColorSpaces.createColorSpace(profile)
: ColorSpaces.getColorSpace(ColorSpace.CS_GRAY);
return ImageTypeSpecifiers.createInterleaved(cs, hasAlpha ? new int[] {1, 0} : new int[] {0}, DataBuffer.TYPE_BYTE, hasAlpha, false);
if (profile != null && profile.getNumComponents() == 4) { case YCbCrA:
return ImageTypeSpecifiers.createInterleaved(ColorSpaces.createColorSpace(profile), new int[]{3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false); case RGBA:
case PhotoYCCA:
hasAlpha = true;
case YCbCr:
case RGB:
case PhotoYCC:
// Create based on PhotoYCC profile...
if (csType == JPEGColorSpace.PhotoYCC || csType == JPEGColorSpace.PhotoYCCA) {
cs = ColorSpaces.getColorSpace(ColorSpace.CS_PYCC);
}
else {
// ...or create based on embedded profile if exists, otherwise create from sRGB
cs = profile != null && profile.getNumComponents() == 3
? ColorSpaces.createColorSpace(profile)
: ColorSpaces.getColorSpace(ColorSpace.CS_sRGB);
} }
return ImageTypeSpecifiers.createInterleaved(ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK), new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false); return ImageTypeSpecifiers.createInterleaved(cs, hasAlpha ? new int[] {3, 2, 1, 0} : new int[] {2, 1, 0}, DataBuffer.TYPE_BYTE, hasAlpha, false);
case YCCK:
case CMYK:
// Create based on embedded profile if exists, otherwise create from "Generic CMYK"
cs = profile != null && profile.getNumComponents() == 4
? ColorSpaces.createColorSpace(profile)
: ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK);
return ImageTypeSpecifiers.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false);
default: default:
// For other types, we probably can't give a proper type, return null // For other types, we probably can't give a proper type
return null; throw new IIOException("Could not determine JPEG source color space");
} }
} }
@ -322,7 +308,8 @@ public final class JPEGImageReader extends ImageReaderBase {
adobeDCT = null; adobeDCT = null;
} }
JPEGColorSpace sourceCSType = getSourceCSType(getJFIF(), adobeDCT, sof); JFIF jfif = getJFIF();
JPEGColorSpace sourceCSType = getSourceCSType(jfif, adobeDCT, sof);
if (sof.marker == JPEG.SOF3) { if (sof.marker == JPEG.SOF3) {
// Read image as lossless // Read image as lossless
@ -347,20 +334,15 @@ public final class JPEGImageReader extends ImageReaderBase {
// We need to apply ICC profile unless the profile is sRGB/default gray (whatever that is) // 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. // - or only filter out the bad ICC profiles in the JPEGSegmentImageInputStream.
else if (delegate.canReadRaster() && ( else if (bogusAdobeDCT
bogusAdobeDCT || || profile != null && !ColorSpaces.isCS_sRGB(profile)
sourceCSType == JPEGColorSpace.CMYK || || (long) sof.lines * sof.samplesPerLine > Integer.MAX_VALUE
sourceCSType == JPEGColorSpace.YCCK || || delegateCSTypeMismatch(jfif, adobeDCT, sof, sourceCSType)) {
profile != null && !ColorSpaces.isCS_sRGB(profile) ||
(long) sof.lines * sof.samplesPerLine > Integer.MAX_VALUE ||
!delegate.getImageTypes(0).hasNext() ||
sourceCSType == JPEGColorSpace.YCbCr && getRawImageType(imageIndex) != null)) { // TODO: Issue warning?
if (DEBUG) { if (DEBUG) {
System.out.println("Reading using raster and extra conversion"); System.out.println("Reading using raster and extra conversion");
System.out.println("ICC color profile: " + profile); System.out.println("ICC color profile: " + profile);
} }
// TODO: Possible to optimize slightly, to avoid readAsRaster for non-CMYK and other good types?
return readImageAsRasterAndReplaceColorProfile(imageIndex, param, sof, sourceCSType, profile); return readImageAsRasterAndReplaceColorProfile(imageIndex, param, sof, sourceCSType, profile);
} }
@ -371,6 +353,56 @@ public final class JPEGImageReader extends ImageReaderBase {
return delegate.read(0, param); return delegate.read(0, param);
} }
private boolean delegateCSTypeMismatch(final JFIF jfif, final AdobeDCT adobeDCT, final Frame startOfFrame, final JPEGColorSpace sourceCSType) throws IOException {
switch (sourceCSType) {
case GrayA:
case RGBA:
case YCbCrA:
case PhotoYCC:
case PhotoYCCA:
case CMYK:
case YCCK:
// These are no longer supported by the delegate, we'll handle ourselves
return true;
}
try {
ImageTypeSpecifier rawImageType = delegate.getRawImageType(0);
switch (sourceCSType) {
case Gray:
return rawImageType == null || rawImageType.getColorModel().getColorSpace().getType() != ColorSpace.TYPE_GRAY;
case YCbCr:
// NOTE: For backwards compatibility, null is allowed for YCbCr
if (rawImageType == null) {
return false;
}
// If We have a JFIF, but with non-standard component Ids, the standard reader mistakes it for RGB
if (jfif != null && (startOfFrame.components[0].id != 1 || startOfFrame.components[1].id != 2 || startOfFrame.components[2].id != 3)) {
return true;
}
// Else, if we have no Adobe marker and no subsampling, the standard reader mistakes it for RGB
else if (adobeDCT == null
&& (startOfFrame.components[0].id != 1 || startOfFrame.components[1].id != 2 || startOfFrame.components[2].id != 3)
&& (startOfFrame.components[0].hSub == 1 || startOfFrame.components[0].vSub == 1
|| startOfFrame.components[1].hSub == 1 || startOfFrame.components[1].vSub == 1
|| startOfFrame.components[2].hSub == 1 || startOfFrame.components[2].vSub == 1)) {
return true;
}
case RGB:
return rawImageType == null || rawImageType.getColorModel().getColorSpace().getType() != ColorSpace.TYPE_RGB;
default:
// Probably needs special handling, but we don't know what to do...
return false;
}
}
catch (IIOException | NullPointerException | ArrayIndexOutOfBoundsException | NegativeArraySizeException ignore) {
// An exception here is a clear indicator we need to handle conversion
return true;
}
}
private BufferedImage readImageAsRasterAndReplaceColorProfile(int imageIndex, ImageReadParam param, Frame startOfFrame, JPEGColorSpace csType, ICC_Profile profile) throws IOException { private BufferedImage readImageAsRasterAndReplaceColorProfile(int imageIndex, ImageReadParam param, Frame startOfFrame, JPEGColorSpace csType, ICC_Profile profile) throws IOException {
int origWidth = getWidth(imageIndex); int origWidth = getWidth(imageIndex);
int origHeight = getHeight(imageIndex); int origHeight = getHeight(imageIndex);
@ -388,7 +420,10 @@ public final class JPEGImageReader extends ImageReaderBase {
RasterOp convert = null; RasterOp convert = null;
ICC_ColorSpace intendedCS = profile != null ? ColorSpaces.createColorSpace(profile) : null; ICC_ColorSpace intendedCS = profile != null ? ColorSpaces.createColorSpace(profile) : null;
if (profile != null && (csType == JPEGColorSpace.Gray || csType == JPEGColorSpace.GrayA)) { if (destination.getNumBands() <= 2 && (csType != JPEGColorSpace.Gray && csType != JPEGColorSpace.GrayA)) {
convert = new LumaToGray();
}
else if (profile != null && (csType == JPEGColorSpace.Gray || csType == JPEGColorSpace.GrayA)) {
// com.sun. reader does not do ColorConvertOp for CS_GRAY, even if embedded ICC profile, // 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)... // probably because IJG native part does it already...? If applied, color looks wrong (too dark)...
// convert = new ColorConvertOp(intendedCS, image.getColorModel().getColorSpace(), null); // convert = new ColorConvertOp(intendedCS, image.getColorModel().getColorSpace(), null);
@ -397,8 +432,7 @@ public final class JPEGImageReader extends ImageReaderBase {
// Handle inconsistencies // Handle inconsistencies
if (startOfFrame.componentsInFrame() != intendedCS.getNumComponents()) { if (startOfFrame.componentsInFrame() != intendedCS.getNumComponents()) {
// If ICC profile number of components and startOfFrame does not match, ignore ICC profile // If ICC profile number of components and startOfFrame does not match, ignore ICC profile
processWarningOccurred(String.format( processWarningOccurred(String.format("Embedded ICC color profile is incompatible with image data. " +
"Embedded ICC color profile is incompatible with image data. " +
"Profile indicates %d components, but SOF%d has %d color components. " + "Profile indicates %d components, but SOF%d has %d color components. " +
"Ignoring ICC profile, assuming source color space %s.", "Ignoring ICC profile, assuming source color space %s.",
intendedCS.getNumComponents(), startOfFrame.marker & 0xf, startOfFrame.componentsInFrame(), csType intendedCS.getNumComponents(), startOfFrame.marker & 0xf, startOfFrame.componentsInFrame(), csType
@ -422,10 +456,7 @@ public final class JPEGImageReader extends ImageReaderBase {
ColorSpace cmykCS = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK); ColorSpace cmykCS = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK);
if (cmykCS instanceof ICC_ColorSpace) { if (cmykCS instanceof ICC_ColorSpace) {
processWarningOccurred( processWarningOccurred("No embedded ICC color profile, defaulting to \"generic\" CMYK ICC profile. Colors may look incorrect.");
"No embedded ICC color profile, defaulting to \"generic\" CMYK ICC profile. " +
"Colors may look incorrect."
);
// NOTE: Avoid using CCOp if same color space, as it's more compatible that way // NOTE: Avoid using CCOp if same color space, as it's more compatible that way
if (cmykCS != image.getColorModel().getColorSpace()) { if (cmykCS != image.getColorModel().getColorSpace()) {
@ -434,17 +465,11 @@ public final class JPEGImageReader extends ImageReaderBase {
} }
else { else {
// ColorConvertOp using non-ICC CS is deadly slow, fall back to fast conversion instead // ColorConvertOp using non-ICC CS is deadly slow, fall back to fast conversion instead
processWarningOccurred( processWarningOccurred("No embedded ICC color profile, will convert using inaccurate CMYK to RGB conversion. Colors may look incorrect.");
"No embedded ICC color profile, will convert using inaccurate CMYK to RGB conversion. " +
"Colors may look incorrect."
);
convert = new FastCMYKToRGB(); convert = new FastCMYKToRGB();
} }
} }
else if (profile != null) {
processWarningOccurred("Embedded ICC color profile is incompatible with Java 2D, color profile will be ignored.");
}
// We'll need a read param // We'll need a read param
if (param == null) { if (param == null) {
@ -523,10 +548,9 @@ public final class JPEGImageReader extends ImageReaderBase {
switch (adobeDCT.transform) { switch (adobeDCT.transform) {
case AdobeDCT.Unknown: case AdobeDCT.Unknown:
return JPEGColorSpace.RGB; return JPEGColorSpace.RGB;
case AdobeDCT.YCC:
return JPEGColorSpace.YCbCr;
default: default:
// TODO: Warning! // TODO: Warning!
case AdobeDCT.YCC:
return JPEGColorSpace.YCbCr; // assume it's YCbCr return JPEGColorSpace.YCbCr; // assume it's YCbCr
} }
} }
@ -556,10 +580,9 @@ public final class JPEGImageReader extends ImageReaderBase {
switch (adobeDCT.transform) { switch (adobeDCT.transform) {
case AdobeDCT.Unknown: case AdobeDCT.Unknown:
return JPEGColorSpace.CMYK; return JPEGColorSpace.CMYK;
case AdobeDCT.YCCK:
return JPEGColorSpace.YCCK;
default: default:
// TODO: Warning! // TODO: Warning!
case AdobeDCT.YCCK:
return JPEGColorSpace.YCCK; // assume it's YCCK return JPEGColorSpace.YCCK; // assume it's YCCK
} }
} }

View File

@ -0,0 +1,65 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.lang.Validate;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.Raster;
import java.awt.image.RasterOp;
import java.awt.image.WritableRaster;
/**
* LumaToGray.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: LumaToGray.java,v 1.0 10/04/2021 haraldk Exp$
*/
final class LumaToGray implements RasterOp {
@Override
public WritableRaster filter(final Raster src, WritableRaster dest) {
Validate.notNull(src, "src may not be null");
Validate.isTrue(src != dest, "src and dest raster may not be same");
Validate.isTrue(src.getNumDataElements() >= 3, src.getNumDataElements(), "Luma raster must have at least 3 data elements: %s");
if (dest == null) {
dest = createCompatibleDestRaster(src);
}
// If src and dest have alpha component, keep it, otherwise extract luma only
int[] bandList = src.getNumBands() > 3 && dest.getNumBands() > 1 ? new int[] {0, 3} : new int[] {0};
dest.setRect(0, 0, src.createChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, bandList));
return dest;
}
@Override
public Rectangle2D getBounds2D(final Raster src) {
return src.getBounds();
}
@Override
public WritableRaster createCompatibleDestRaster(final Raster src) {
WritableRaster raster = src.createCompatibleWritableRaster();
return raster.createWritableChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, new int[] {0});
}
@Override
public Point2D getPoint2D(final Point2D srcPt, Point2D dstPt) {
if (dstPt == null) {
dstPt = new Point2D.Double(srcPt.getX(), srcPt.getY());
}
else {
dstPt.setLocation(srcPt);
}
return dstPt;
}
@Override
public RenderingHints getRenderingHints() {
return null;
}
}

View File

@ -63,7 +63,7 @@ public class FastCMYKToRGBTest {
assertNotNull(pixel); assertNotNull(pixel);
assertEquals(3, pixel.length); assertEquals(3, pixel.length);
byte[] expected = {(byte) 255, (byte) 255, (byte) 255}; byte[] expected = {(byte) 255, (byte) 255, (byte) 255};
assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); assertArrayEquals(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), expected, pixel);
} }
@Test @Test
@ -95,7 +95,7 @@ public class FastCMYKToRGBTest {
assertNotNull(pixel); assertNotNull(pixel);
assertEquals(3, pixel.length); assertEquals(3, pixel.length);
byte[] expected = {(byte) 0, (byte) 0, (byte) 0}; byte[] expected = {(byte) 0, (byte) 0, (byte) 0};
assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); assertArrayEquals(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), expected, pixel);
} }
} }
@ -134,7 +134,7 @@ public class FastCMYKToRGBTest {
assertNotNull(pixel); assertNotNull(pixel);
assertEquals(3, pixel.length); assertEquals(3, pixel.length);
byte[] expected = {(byte) (255 - i), (byte) i, (byte) (127 - i)}; byte[] expected = {(byte) (255 - i), (byte) i, (byte) (127 - i)};
assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); assertArrayEquals(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), expected, pixel);
} }
} }
@ -153,7 +153,7 @@ public class FastCMYKToRGBTest {
assertNotNull(pixel); assertNotNull(pixel);
assertEquals(3, pixel.length); assertEquals(3, pixel.length);
byte[] expected = {(byte) (255 - i), (byte) i, (byte) (127 - i)}; byte[] expected = {(byte) (255 - i), (byte) i, (byte) (127 - i)};
assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); assertArrayEquals(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), expected, pixel);
} }
} }
@ -172,7 +172,7 @@ public class FastCMYKToRGBTest {
assertNotNull(pixel); assertNotNull(pixel);
assertEquals(4, pixel.length); assertEquals(4, pixel.length);
byte[] expected = {(byte) (255 - i), (byte) i, (byte) (127 - i), (byte) 0xff}; byte[] expected = {(byte) (255 - i), (byte) i, (byte) (127 - i), (byte) 0xff};
assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); assertArrayEquals(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), expected, pixel);
} }
} }

View File

@ -31,6 +31,7 @@
package com.twelvemonkeys.imageio.plugins.jpeg; package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.lang.StringUtil; import com.twelvemonkeys.lang.StringUtil;
import org.hamcrest.core.IsInstanceOf; import org.hamcrest.core.IsInstanceOf;
@ -55,6 +56,7 @@ import java.awt.*;
import java.awt.color.ColorSpace; import java.awt.color.ColorSpace;
import java.awt.color.ICC_Profile; import java.awt.color.ICC_Profile;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte; import java.awt.image.DataBufferByte;
import java.io.*; import java.io.*;
import java.util.List; import java.util.List;
@ -140,11 +142,6 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest<JPEGImageReader
// More test data in specific tests below // More test data in specific tests below
} }
@Override
protected boolean allowsNullRawImageType() {
return true;
}
@Override @Override
protected List<String> getFormatNames() { protected List<String> getFormatNames() {
return Arrays.asList("JPEG", "jpeg", "JPG", "jpg", return Arrays.asList("JPEG", "jpeg", "JPG", "jpg",
@ -422,8 +419,8 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest<JPEGImageReader
} }
@Test @Test
public void testYCbCrNotSubsampledNonstandardChannelIndexes() throws IOException { public void testYCbCrNotSubsampledNonstandardComponentIds() throws IOException {
// Regression: Make sure 3 channel, non-subsampled JFIF, defaults to YCbCr, even if unstandard channel indexes // Regression: Make sure 3 channel, non-subsampled JFIF, defaults to YCbCr, even if nonstandard component ids
JPEGImageReader reader = createReader(); JPEGImageReader reader = createReader();
try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-ycbcr-no-subsampling-intel.jpg"))) { try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-ycbcr-no-subsampling-intel.jpg"))) {
reader.setInput(stream); reader.setInput(stream);
@ -1235,6 +1232,56 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest<JPEGImageReader
} }
} }
@Test
public void testRGBANoGrayImageTypes() throws IOException {
JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/adobe-unknown-rgb-ids.jpg")));
Iterator<ImageTypeSpecifier> imageTypes = reader.getImageTypes(0);
while (imageTypes.hasNext()) {
ImageTypeSpecifier specifier = imageTypes.next();
assertNotEquals("RGB JPEGs can't be decoded as Gray as it has no luminance (Y) component", ColorSpace.TYPE_GRAY, specifier.getColorModel().getColorSpace().getType());
}
reader.dispose();
}
@Test(expected = Exception.class)
public void testRGBAsGray() throws IOException {
final JPEGImageReader reader = createReader();
try {
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/adobe-unknown-rgb-ids.jpg")));
assertEquals(225, reader.getWidth(0));
assertEquals(156, reader.getHeight(0));
final ImageReadParam param = reader.getDefaultReadParam();
param.setSourceRegion(new Rectangle(0, 0, 225, 8));
param.setDestinationType(ImageTypeSpecifiers.createGrayscale(8, DataBuffer.TYPE_BYTE));
// Should ideally throw IIOException due to destination type mismatch, but throws IllegalArgumentException...
reader.read(0, param);
}
finally {
reader.dispose();
}
}
@Test
public void testYCbCrAsGray() throws IOException {
JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-ycbcr-no-subsampling-intel.jpg")));
ImageReadParam param = reader.getDefaultReadParam();
param.setDestinationType(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY));
BufferedImage image = reader.read(0, param);
assertNotNull(image);
assertEquals(BufferedImage.TYPE_BYTE_GRAY, image.getType());
}
/** /**
* Slightly fuzzy RGB equals method. Tolerance +/-5 steps. * Slightly fuzzy RGB equals method. Tolerance +/-5 steps.
*/ */
@ -1818,7 +1865,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest<JPEGImageReader
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/exif-jfif-app13-app14ycck-3channel.jpg"))); reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/exif-jfif-app13-app14ycck-3channel.jpg")));
ImageTypeSpecifier rawType = reader.getRawImageType(0); ImageTypeSpecifier rawType = reader.getRawImageType(0);
assertNull(rawType); // But no exception, please... assertNotNull(rawType); // As of Java 9, use RGB for YCC and CMYK for YCCK
} }
finally { finally {
reader.dispose(); reader.dispose();

View File

@ -0,0 +1,79 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import org.junit.Test;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.util.Arrays;
import static org.junit.Assert.*;
/**
* LumaToGrayTest.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: LumaToGrayTest.java,v 1.0 10/04/2021 haraldk Exp$
*/
public class LumaToGrayTest {
@Test
public void testConvertByteYcc() {
LumaToGray convert = new LumaToGray();
WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 3, null);
WritableRaster result = null;
byte[] pixel = null;
for (int i = 0; i < 255; i++) {
input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (127 + i)});
result = convert.filter(input, result);
pixel = (byte[]) result.getDataElements(0, 0, pixel);
assertNotNull(pixel);
assertEquals(1, pixel.length);
byte[] expected = {(byte) i};
assertArrayEquals(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), expected, pixel);
}
}
@Test
public void testConvertByteYccK() {
LumaToGray convert = new LumaToGray();
WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null);
WritableRaster result = null;
byte[] pixel = null;
for (int i = 0; i < 255; i++) {
input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (127 + i), (byte) 255});
result = convert.filter(input, result);
pixel = (byte[]) result.getDataElements(0, 0, pixel);
assertNotNull(pixel);
assertEquals(1, pixel.length);
byte[] expected = {(byte) i};
assertArrayEquals(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), expected, pixel);
}
}
@Test
public void testConvertByteYccA() {
LumaToGray convert = new LumaToGray();
WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null);
WritableRaster result = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 2, null);
byte[] pixel = null;
for (int i = 0; i < 255; i++) {
input.setDataElements(0, 0, new byte[] {(byte) i, (byte) 255, (byte) (127 + i), (byte) (255 - i)});
result = convert.filter(input, result);
pixel = (byte[]) result.getDataElements(0, 0, pixel);
assertNotNull(pixel);
assertEquals(2, pixel.length);
byte[] expected = {(byte) i, (byte) (255 - i)};
assertArrayEquals(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), expected, pixel);
}
}
}