You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1116 lines
44 KiB
1116 lines
44 KiB
/*
|
|
* Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
|
|
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*/
|
|
|
|
/*
|
|
**********************************************************************
|
|
**********************************************************************
|
|
**********************************************************************
|
|
*** COPYRIGHT (c) Eastman Kodak Company, 1997 ***
|
|
*** As an unpublished work pursuant to Title 17 of the United ***
|
|
*** States Code. All rights reserved. ***
|
|
**********************************************************************
|
|
**********************************************************************
|
|
**********************************************************************/
|
|
|
|
package java.awt.image;
|
|
|
|
import java.awt.Point;
|
|
import java.awt.Graphics2D;
|
|
import java.awt.color.*;
|
|
import sun.java2d.cmm.ColorTransform;
|
|
import sun.java2d.cmm.CMSManager;
|
|
import sun.java2d.cmm.ProfileDeferralMgr;
|
|
import sun.java2d.cmm.PCMM;
|
|
import java.awt.geom.Rectangle2D;
|
|
import java.awt.geom.Point2D;
|
|
import java.awt.RenderingHints;
|
|
|
|
/**
|
|
* This class performs a pixel-by-pixel color conversion of the data in
|
|
* the source image. The resulting color values are scaled to the precision
|
|
* of the destination image. Color conversion can be specified
|
|
* via an array of ColorSpace objects or an array of ICC_Profile objects.
|
|
* <p>
|
|
* If the source is a BufferedImage with premultiplied alpha, the
|
|
* color components are divided by the alpha component before color conversion.
|
|
* If the destination is a BufferedImage with premultiplied alpha, the
|
|
* color components are multiplied by the alpha component after conversion.
|
|
* Rasters are treated as having no alpha channel, i.e. all bands are
|
|
* color bands.
|
|
* <p>
|
|
* If a RenderingHints object is specified in the constructor, the
|
|
* color rendering hint and the dithering hint may be used to control
|
|
* color conversion.
|
|
* <p>
|
|
* Note that Source and Destination may be the same object.
|
|
* @see java.awt.RenderingHints#KEY_COLOR_RENDERING
|
|
* @see java.awt.RenderingHints#KEY_DITHERING
|
|
*/
|
|
public class ColorConvertOp implements BufferedImageOp, RasterOp {
|
|
ICC_Profile[] profileList;
|
|
ColorSpace[] CSList;
|
|
ColorTransform thisTransform, thisRasterTransform;
|
|
ICC_Profile thisSrcProfile, thisDestProfile;
|
|
RenderingHints hints;
|
|
boolean gotProfiles;
|
|
float[] srcMinVals, srcMaxVals, dstMinVals, dstMaxVals;
|
|
|
|
/* the class initializer */
|
|
static {
|
|
if (ProfileDeferralMgr.deferring) {
|
|
ProfileDeferralMgr.activateProfiles();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constructs a new ColorConvertOp which will convert
|
|
* from a source color space to a destination color space.
|
|
* The RenderingHints argument may be null.
|
|
* This Op can be used only with BufferedImages, and will convert
|
|
* directly from the ColorSpace of the source image to that of the
|
|
* destination. The destination argument of the filter method
|
|
* cannot be specified as null.
|
|
* @param hints the <code>RenderingHints</code> object used to control
|
|
* the color conversion, or <code>null</code>
|
|
*/
|
|
public ColorConvertOp (RenderingHints hints)
|
|
{
|
|
profileList = new ICC_Profile [0]; /* 0 length list */
|
|
this.hints = hints;
|
|
}
|
|
|
|
/**
|
|
* Constructs a new ColorConvertOp from a ColorSpace object.
|
|
* The RenderingHints argument may be null. This
|
|
* Op can be used only with BufferedImages, and is primarily useful
|
|
* when the {@link #filter(BufferedImage, BufferedImage) filter}
|
|
* method is invoked with a destination argument of null.
|
|
* In that case, the ColorSpace defines the destination color space
|
|
* for the destination created by the filter method. Otherwise, the
|
|
* ColorSpace defines an intermediate space to which the source is
|
|
* converted before being converted to the destination space.
|
|
* @param cspace defines the destination <code>ColorSpace</code> or an
|
|
* intermediate <code>ColorSpace</code>
|
|
* @param hints the <code>RenderingHints</code> object used to control
|
|
* the color conversion, or <code>null</code>
|
|
* @throws NullPointerException if cspace is null
|
|
*/
|
|
public ColorConvertOp (ColorSpace cspace, RenderingHints hints)
|
|
{
|
|
if (cspace == null) {
|
|
throw new NullPointerException("ColorSpace cannot be null");
|
|
}
|
|
if (cspace instanceof ICC_ColorSpace) {
|
|
profileList = new ICC_Profile [1]; /* 1 profile in the list */
|
|
|
|
profileList [0] = ((ICC_ColorSpace) cspace).getProfile();
|
|
}
|
|
else {
|
|
CSList = new ColorSpace[1]; /* non-ICC case: 1 ColorSpace in list */
|
|
CSList[0] = cspace;
|
|
}
|
|
this.hints = hints;
|
|
}
|
|
|
|
|
|
/**
|
|
* Constructs a new ColorConvertOp from two ColorSpace objects.
|
|
* The RenderingHints argument may be null.
|
|
* This Op is primarily useful for calling the filter method on
|
|
* Rasters, in which case the two ColorSpaces define the operation
|
|
* to be performed on the Rasters. In that case, the number of bands
|
|
* in the source Raster must match the number of components in
|
|
* srcCspace, and the number of bands in the destination Raster
|
|
* must match the number of components in dstCspace. For BufferedImages,
|
|
* the two ColorSpaces define intermediate spaces through which the
|
|
* source is converted before being converted to the destination space.
|
|
* @param srcCspace the source <code>ColorSpace</code>
|
|
* @param dstCspace the destination <code>ColorSpace</code>
|
|
* @param hints the <code>RenderingHints</code> object used to control
|
|
* the color conversion, or <code>null</code>
|
|
* @throws NullPointerException if either srcCspace or dstCspace is null
|
|
*/
|
|
public ColorConvertOp(ColorSpace srcCspace, ColorSpace dstCspace,
|
|
RenderingHints hints)
|
|
{
|
|
if ((srcCspace == null) || (dstCspace == null)) {
|
|
throw new NullPointerException("ColorSpaces cannot be null");
|
|
}
|
|
if ((srcCspace instanceof ICC_ColorSpace) &&
|
|
(dstCspace instanceof ICC_ColorSpace)) {
|
|
profileList = new ICC_Profile [2]; /* 2 profiles in the list */
|
|
|
|
profileList [0] = ((ICC_ColorSpace) srcCspace).getProfile();
|
|
profileList [1] = ((ICC_ColorSpace) dstCspace).getProfile();
|
|
|
|
getMinMaxValsFromColorSpaces(srcCspace, dstCspace);
|
|
} else {
|
|
/* non-ICC case: 2 ColorSpaces in list */
|
|
CSList = new ColorSpace[2];
|
|
CSList[0] = srcCspace;
|
|
CSList[1] = dstCspace;
|
|
}
|
|
this.hints = hints;
|
|
}
|
|
|
|
|
|
/**
|
|
* Constructs a new ColorConvertOp from an array of ICC_Profiles.
|
|
* The RenderingHints argument may be null.
|
|
* The sequence of profiles may include profiles that represent color
|
|
* spaces, profiles that represent effects, etc. If the whole sequence
|
|
* does not represent a well-defined color conversion, an exception is
|
|
* thrown.
|
|
* <p>For BufferedImages, if the ColorSpace
|
|
* of the source BufferedImage does not match the requirements of the
|
|
* first profile in the array,
|
|
* the first conversion is to an appropriate ColorSpace.
|
|
* If the requirements of the last profile in the array are not met
|
|
* by the ColorSpace of the destination BufferedImage,
|
|
* the last conversion is to the destination's ColorSpace.
|
|
* <p>For Rasters, the number of bands in the source Raster must match
|
|
* the requirements of the first profile in the array, and the
|
|
* number of bands in the destination Raster must match the requirements
|
|
* of the last profile in the array. The array must have at least two
|
|
* elements or calling the filter method for Rasters will throw an
|
|
* IllegalArgumentException.
|
|
* @param profiles the array of <code>ICC_Profile</code> objects
|
|
* @param hints the <code>RenderingHints</code> object used to control
|
|
* the color conversion, or <code>null</code>
|
|
* @exception IllegalArgumentException when the profile sequence does not
|
|
* specify a well-defined color conversion
|
|
* @exception NullPointerException if profiles is null
|
|
*/
|
|
public ColorConvertOp (ICC_Profile[] profiles, RenderingHints hints)
|
|
{
|
|
if (profiles == null) {
|
|
throw new NullPointerException("Profiles cannot be null");
|
|
}
|
|
gotProfiles = true;
|
|
profileList = new ICC_Profile[profiles.length];
|
|
for (int i1 = 0; i1 < profiles.length; i1++) {
|
|
profileList[i1] = profiles[i1];
|
|
}
|
|
this.hints = hints;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the array of ICC_Profiles used to construct this ColorConvertOp.
|
|
* Returns null if the ColorConvertOp was not constructed from such an
|
|
* array.
|
|
* @return the array of <code>ICC_Profile</code> objects of this
|
|
* <code>ColorConvertOp</code>, or <code>null</code> if this
|
|
* <code>ColorConvertOp</code> was not constructed with an
|
|
* array of <code>ICC_Profile</code> objects.
|
|
*/
|
|
public final ICC_Profile[] getICC_Profiles() {
|
|
if (gotProfiles) {
|
|
ICC_Profile[] profiles = new ICC_Profile[profileList.length];
|
|
for (int i1 = 0; i1 < profileList.length; i1++) {
|
|
profiles[i1] = profileList[i1];
|
|
}
|
|
return profiles;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* ColorConverts the source BufferedImage.
|
|
* If the destination image is null,
|
|
* a BufferedImage will be created with an appropriate ColorModel.
|
|
* @param src the source <code>BufferedImage</code> to be converted
|
|
* @param dest the destination <code>BufferedImage</code>,
|
|
* or <code>null</code>
|
|
* @return <code>dest</code> color converted from <code>src</code>
|
|
* or a new, converted <code>BufferedImage</code>
|
|
* if <code>dest</code> is <code>null</code>
|
|
* @exception IllegalArgumentException if dest is null and this op was
|
|
* constructed using the constructor which takes only a
|
|
* RenderingHints argument, since the operation is ill defined.
|
|
*/
|
|
public final BufferedImage filter(BufferedImage src, BufferedImage dest) {
|
|
ColorSpace srcColorSpace, destColorSpace;
|
|
BufferedImage savdest = null;
|
|
|
|
if (src.getColorModel() instanceof IndexColorModel) {
|
|
IndexColorModel icm = (IndexColorModel) src.getColorModel();
|
|
src = icm.convertToIntDiscrete(src.getRaster(), true);
|
|
}
|
|
srcColorSpace = src.getColorModel().getColorSpace();
|
|
if (dest != null) {
|
|
if (dest.getColorModel() instanceof IndexColorModel) {
|
|
savdest = dest;
|
|
dest = null;
|
|
destColorSpace = null;
|
|
} else {
|
|
destColorSpace = dest.getColorModel().getColorSpace();
|
|
}
|
|
} else {
|
|
destColorSpace = null;
|
|
}
|
|
|
|
if ((CSList != null) ||
|
|
(!(srcColorSpace instanceof ICC_ColorSpace)) ||
|
|
((dest != null) &&
|
|
(!(destColorSpace instanceof ICC_ColorSpace)))) {
|
|
/* non-ICC case */
|
|
dest = nonICCBIFilter(src, srcColorSpace, dest, destColorSpace);
|
|
} else {
|
|
dest = ICCBIFilter(src, srcColorSpace, dest, destColorSpace);
|
|
}
|
|
|
|
if (savdest != null) {
|
|
Graphics2D big = savdest.createGraphics();
|
|
try {
|
|
big.drawImage(dest, 0, 0, null);
|
|
} finally {
|
|
big.dispose();
|
|
}
|
|
return savdest;
|
|
} else {
|
|
return dest;
|
|
}
|
|
}
|
|
|
|
private final BufferedImage ICCBIFilter(BufferedImage src,
|
|
ColorSpace srcColorSpace,
|
|
BufferedImage dest,
|
|
ColorSpace destColorSpace) {
|
|
int nProfiles = profileList.length;
|
|
ICC_Profile srcProfile = null, destProfile = null;
|
|
|
|
srcProfile = ((ICC_ColorSpace) srcColorSpace).getProfile();
|
|
|
|
if (dest == null) { /* last profile in the list defines
|
|
the output color space */
|
|
if (nProfiles == 0) {
|
|
throw new IllegalArgumentException(
|
|
"Destination ColorSpace is undefined");
|
|
}
|
|
destProfile = profileList [nProfiles - 1];
|
|
dest = createCompatibleDestImage(src, null);
|
|
}
|
|
else {
|
|
if (src.getHeight() != dest.getHeight() ||
|
|
src.getWidth() != dest.getWidth()) {
|
|
throw new IllegalArgumentException(
|
|
"Width or height of BufferedImages do not match");
|
|
}
|
|
destProfile = ((ICC_ColorSpace) destColorSpace).getProfile();
|
|
}
|
|
|
|
/* Checking if all profiles in the transform sequence are the same.
|
|
* If so, performing just copying the data.
|
|
*/
|
|
if (srcProfile == destProfile) {
|
|
boolean noTrans = true;
|
|
for (int i = 0; i < nProfiles; i++) {
|
|
if (srcProfile != profileList[i]) {
|
|
noTrans = false;
|
|
break;
|
|
}
|
|
}
|
|
if (noTrans) {
|
|
Graphics2D g = dest.createGraphics();
|
|
try {
|
|
g.drawImage(src, 0, 0, null);
|
|
} finally {
|
|
g.dispose();
|
|
}
|
|
|
|
return dest;
|
|
}
|
|
}
|
|
|
|
/* make a new transform if needed */
|
|
if ((thisTransform == null) || (thisSrcProfile != srcProfile) ||
|
|
(thisDestProfile != destProfile) ) {
|
|
updateBITransform(srcProfile, destProfile);
|
|
}
|
|
|
|
/* color convert the image */
|
|
thisTransform.colorConvert(src, dest);
|
|
|
|
return dest;
|
|
}
|
|
|
|
private void updateBITransform(ICC_Profile srcProfile,
|
|
ICC_Profile destProfile) {
|
|
ICC_Profile[] theProfiles;
|
|
int i1, nProfiles, nTransforms, whichTrans, renderState;
|
|
ColorTransform[] theTransforms;
|
|
boolean useSrc = false, useDest = false;
|
|
|
|
nProfiles = profileList.length;
|
|
nTransforms = nProfiles;
|
|
if ((nProfiles == 0) || (srcProfile != profileList[0])) {
|
|
nTransforms += 1;
|
|
useSrc = true;
|
|
}
|
|
if ((nProfiles == 0) || (destProfile != profileList[nProfiles - 1]) ||
|
|
(nTransforms < 2)) {
|
|
nTransforms += 1;
|
|
useDest = true;
|
|
}
|
|
|
|
/* make the profile list */
|
|
theProfiles = new ICC_Profile[nTransforms]; /* the list of profiles
|
|
for this Op */
|
|
|
|
int idx = 0;
|
|
if (useSrc) {
|
|
/* insert source as first profile */
|
|
theProfiles[idx++] = srcProfile;
|
|
}
|
|
|
|
for (i1 = 0; i1 < nProfiles; i1++) {
|
|
/* insert profiles defined in this Op */
|
|
theProfiles[idx++] = profileList [i1];
|
|
}
|
|
|
|
if (useDest) {
|
|
/* insert dest as last profile */
|
|
theProfiles[idx] = destProfile;
|
|
}
|
|
|
|
/* make the transform list */
|
|
theTransforms = new ColorTransform [nTransforms];
|
|
|
|
/* initialize transform get loop */
|
|
if (theProfiles[0].getProfileClass() == ICC_Profile.CLASS_OUTPUT) {
|
|
/* if first profile is a printer
|
|
render as colorimetric */
|
|
renderState = ICC_Profile.icRelativeColorimetric;
|
|
}
|
|
else {
|
|
renderState = ICC_Profile.icPerceptual; /* render any other
|
|
class perceptually */
|
|
}
|
|
|
|
whichTrans = ColorTransform.In;
|
|
|
|
PCMM mdl = CMSManager.getModule();
|
|
|
|
/* get the transforms from each profile */
|
|
for (i1 = 0; i1 < nTransforms; i1++) {
|
|
if (i1 == nTransforms -1) { /* last profile? */
|
|
whichTrans = ColorTransform.Out; /* get output transform */
|
|
}
|
|
else { /* check for abstract profile */
|
|
if ((whichTrans == ColorTransform.Simulation) &&
|
|
(theProfiles[i1].getProfileClass () ==
|
|
ICC_Profile.CLASS_ABSTRACT)) {
|
|
renderState = ICC_Profile.icPerceptual;
|
|
whichTrans = ColorTransform.In;
|
|
}
|
|
}
|
|
|
|
theTransforms[i1] = mdl.createTransform (
|
|
theProfiles[i1], renderState, whichTrans);
|
|
|
|
/* get this profile's rendering intent to select transform
|
|
from next profile */
|
|
renderState = getRenderingIntent(theProfiles[i1]);
|
|
|
|
/* "middle" profiles use simulation transform */
|
|
whichTrans = ColorTransform.Simulation;
|
|
}
|
|
|
|
/* make the net transform */
|
|
thisTransform = mdl.createTransform(theTransforms);
|
|
|
|
/* update corresponding source and dest profiles */
|
|
thisSrcProfile = srcProfile;
|
|
thisDestProfile = destProfile;
|
|
}
|
|
|
|
/**
|
|
* ColorConverts the image data in the source Raster.
|
|
* If the destination Raster is null, a new Raster will be created.
|
|
* The number of bands in the source and destination Rasters must
|
|
* meet the requirements explained above. The constructor used to
|
|
* create this ColorConvertOp must have provided enough information
|
|
* to define both source and destination color spaces. See above.
|
|
* Otherwise, an exception is thrown.
|
|
* @param src the source <code>Raster</code> to be converted
|
|
* @param dest the destination <code>WritableRaster</code>,
|
|
* or <code>null</code>
|
|
* @return <code>dest</code> color converted from <code>src</code>
|
|
* or a new, converted <code>WritableRaster</code>
|
|
* if <code>dest</code> is <code>null</code>
|
|
* @exception IllegalArgumentException if the number of source or
|
|
* destination bands is incorrect, the source or destination
|
|
* color spaces are undefined, or this op was constructed
|
|
* with one of the constructors that applies only to
|
|
* operations on BufferedImages.
|
|
*/
|
|
public final WritableRaster filter (Raster src, WritableRaster dest) {
|
|
|
|
if (CSList != null) {
|
|
/* non-ICC case */
|
|
return nonICCRasterFilter(src, dest);
|
|
}
|
|
int nProfiles = profileList.length;
|
|
if (nProfiles < 2) {
|
|
throw new IllegalArgumentException(
|
|
"Source or Destination ColorSpace is undefined");
|
|
}
|
|
if (src.getNumBands() != profileList[0].getNumComponents()) {
|
|
throw new IllegalArgumentException(
|
|
"Numbers of source Raster bands and source color space " +
|
|
"components do not match");
|
|
}
|
|
if (dest == null) {
|
|
dest = createCompatibleDestRaster(src);
|
|
}
|
|
else {
|
|
if (src.getHeight() != dest.getHeight() ||
|
|
src.getWidth() != dest.getWidth()) {
|
|
throw new IllegalArgumentException(
|
|
"Width or height of Rasters do not match");
|
|
}
|
|
if (dest.getNumBands() !=
|
|
profileList[nProfiles-1].getNumComponents()) {
|
|
throw new IllegalArgumentException(
|
|
"Numbers of destination Raster bands and destination " +
|
|
"color space components do not match");
|
|
}
|
|
}
|
|
|
|
/* make a new transform if needed */
|
|
if (thisRasterTransform == null) {
|
|
int i1, whichTrans, renderState;
|
|
ColorTransform[] theTransforms;
|
|
|
|
/* make the transform list */
|
|
theTransforms = new ColorTransform [nProfiles];
|
|
|
|
/* initialize transform get loop */
|
|
if (profileList[0].getProfileClass() == ICC_Profile.CLASS_OUTPUT) {
|
|
/* if first profile is a printer
|
|
render as colorimetric */
|
|
renderState = ICC_Profile.icRelativeColorimetric;
|
|
}
|
|
else {
|
|
renderState = ICC_Profile.icPerceptual; /* render any other
|
|
class perceptually */
|
|
}
|
|
|
|
whichTrans = ColorTransform.In;
|
|
|
|
PCMM mdl = CMSManager.getModule();
|
|
|
|
/* get the transforms from each profile */
|
|
for (i1 = 0; i1 < nProfiles; i1++) {
|
|
if (i1 == nProfiles -1) { /* last profile? */
|
|
whichTrans = ColorTransform.Out; /* get output transform */
|
|
}
|
|
else { /* check for abstract profile */
|
|
if ((whichTrans == ColorTransform.Simulation) &&
|
|
(profileList[i1].getProfileClass () ==
|
|
ICC_Profile.CLASS_ABSTRACT)) {
|
|
renderState = ICC_Profile.icPerceptual;
|
|
whichTrans = ColorTransform.In;
|
|
}
|
|
}
|
|
|
|
theTransforms[i1] = mdl.createTransform (
|
|
profileList[i1], renderState, whichTrans);
|
|
|
|
/* get this profile's rendering intent to select transform
|
|
from next profile */
|
|
renderState = getRenderingIntent(profileList[i1]);
|
|
|
|
/* "middle" profiles use simulation transform */
|
|
whichTrans = ColorTransform.Simulation;
|
|
}
|
|
|
|
/* make the net transform */
|
|
thisRasterTransform = mdl.createTransform(theTransforms);
|
|
}
|
|
|
|
int srcTransferType = src.getTransferType();
|
|
int dstTransferType = dest.getTransferType();
|
|
if ((srcTransferType == DataBuffer.TYPE_FLOAT) ||
|
|
(srcTransferType == DataBuffer.TYPE_DOUBLE) ||
|
|
(dstTransferType == DataBuffer.TYPE_FLOAT) ||
|
|
(dstTransferType == DataBuffer.TYPE_DOUBLE)) {
|
|
if (srcMinVals == null) {
|
|
getMinMaxValsFromProfiles(profileList[0],
|
|
profileList[nProfiles-1]);
|
|
}
|
|
/* color convert the raster */
|
|
thisRasterTransform.colorConvert(src, dest,
|
|
srcMinVals, srcMaxVals,
|
|
dstMinVals, dstMaxVals);
|
|
} else {
|
|
/* color convert the raster */
|
|
thisRasterTransform.colorConvert(src, dest);
|
|
}
|
|
|
|
|
|
return dest;
|
|
}
|
|
|
|
/**
|
|
* Returns the bounding box of the destination, given this source.
|
|
* Note that this will be the same as the the bounding box of the
|
|
* source.
|
|
* @param src the source <code>BufferedImage</code>
|
|
* @return a <code>Rectangle2D</code> that is the bounding box
|
|
* of the destination, given the specified <code>src</code>
|
|
*/
|
|
public final Rectangle2D getBounds2D (BufferedImage src) {
|
|
return getBounds2D(src.getRaster());
|
|
}
|
|
|
|
/**
|
|
* Returns the bounding box of the destination, given this source.
|
|
* Note that this will be the same as the the bounding box of the
|
|
* source.
|
|
* @param src the source <code>Raster</code>
|
|
* @return a <code>Rectangle2D</code> that is the bounding box
|
|
* of the destination, given the specified <code>src</code>
|
|
*/
|
|
public final Rectangle2D getBounds2D (Raster src) {
|
|
/* return new Rectangle (src.getXOffset(),
|
|
src.getYOffset(),
|
|
src.getWidth(), src.getHeight()); */
|
|
return src.getBounds();
|
|
}
|
|
|
|
/**
|
|
* Creates a zeroed destination image with the correct size and number of
|
|
* bands, given this source.
|
|
* @param src Source image for the filter operation.
|
|
* @param destCM ColorModel of the destination. If null, an
|
|
* appropriate ColorModel will be used.
|
|
* @return a <code>BufferedImage</code> with the correct size and
|
|
* number of bands from the specified <code>src</code>.
|
|
* @throws IllegalArgumentException if <code>destCM</code> is
|
|
* <code>null</code> and this <code>ColorConvertOp</code> was
|
|
* created without any <code>ICC_Profile</code> or
|
|
* <code>ColorSpace</code> defined for the destination
|
|
*/
|
|
public BufferedImage createCompatibleDestImage (BufferedImage src,
|
|
ColorModel destCM) {
|
|
ColorSpace cs = null;;
|
|
if (destCM == null) {
|
|
if (CSList == null) {
|
|
/* ICC case */
|
|
int nProfiles = profileList.length;
|
|
if (nProfiles == 0) {
|
|
throw new IllegalArgumentException(
|
|
"Destination ColorSpace is undefined");
|
|
}
|
|
ICC_Profile destProfile = profileList[nProfiles - 1];
|
|
cs = new ICC_ColorSpace(destProfile);
|
|
} else {
|
|
/* non-ICC case */
|
|
int nSpaces = CSList.length;
|
|
cs = CSList[nSpaces - 1];
|
|
}
|
|
}
|
|
return createCompatibleDestImage(src, destCM, cs);
|
|
}
|
|
|
|
private BufferedImage createCompatibleDestImage(BufferedImage src,
|
|
ColorModel destCM,
|
|
ColorSpace destCS) {
|
|
BufferedImage image;
|
|
if (destCM == null) {
|
|
ColorModel srcCM = src.getColorModel();
|
|
int nbands = destCS.getNumComponents();
|
|
boolean hasAlpha = srcCM.hasAlpha();
|
|
if (hasAlpha) {
|
|
nbands += 1;
|
|
}
|
|
int[] nbits = new int[nbands];
|
|
for (int i = 0; i < nbands; i++) {
|
|
nbits[i] = 8;
|
|
}
|
|
destCM = new ComponentColorModel(destCS, nbits, hasAlpha,
|
|
srcCM.isAlphaPremultiplied(),
|
|
srcCM.getTransparency(),
|
|
DataBuffer.TYPE_BYTE);
|
|
}
|
|
int w = src.getWidth();
|
|
int h = src.getHeight();
|
|
image = new BufferedImage(destCM,
|
|
destCM.createCompatibleWritableRaster(w, h),
|
|
destCM.isAlphaPremultiplied(), null);
|
|
return image;
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates a zeroed destination Raster with the correct size and number of
|
|
* bands, given this source.
|
|
* @param src the specified <code>Raster</code>
|
|
* @return a <code>WritableRaster</code> with the correct size and number
|
|
* of bands from the specified <code>src</code>
|
|
* @throws IllegalArgumentException if this <code>ColorConvertOp</code>
|
|
* was created without sufficient information to define the
|
|
* <code>dst</code> and <code>src</code> color spaces
|
|
*/
|
|
public WritableRaster createCompatibleDestRaster (Raster src) {
|
|
int ncomponents;
|
|
|
|
if (CSList != null) {
|
|
/* non-ICC case */
|
|
if (CSList.length != 2) {
|
|
throw new IllegalArgumentException(
|
|
"Destination ColorSpace is undefined");
|
|
}
|
|
ncomponents = CSList[1].getNumComponents();
|
|
} else {
|
|
/* ICC case */
|
|
int nProfiles = profileList.length;
|
|
if (nProfiles < 2) {
|
|
throw new IllegalArgumentException(
|
|
"Destination ColorSpace is undefined");
|
|
}
|
|
ncomponents = profileList[nProfiles-1].getNumComponents();
|
|
}
|
|
|
|
WritableRaster dest =
|
|
Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE,
|
|
src.getWidth(),
|
|
src.getHeight(),
|
|
ncomponents,
|
|
new Point(src.getMinX(), src.getMinY()));
|
|
return dest;
|
|
}
|
|
|
|
/**
|
|
* Returns the location of the destination point given a
|
|
* point in the source. If <code>dstPt</code> is non-null,
|
|
* it will be used to hold the return value. Note that
|
|
* for this class, the destination point will be the same
|
|
* as the source point.
|
|
* @param srcPt the specified source <code>Point2D</code>
|
|
* @param dstPt the destination <code>Point2D</code>
|
|
* @return <code>dstPt</code> after setting its location to be
|
|
* the same as <code>srcPt</code>
|
|
*/
|
|
public final Point2D getPoint2D (Point2D srcPt, Point2D dstPt) {
|
|
if (dstPt == null) {
|
|
dstPt = new Point2D.Float();
|
|
}
|
|
dstPt.setLocation(srcPt.getX(), srcPt.getY());
|
|
|
|
return dstPt;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the RenderingIntent from the specified ICC Profile.
|
|
*/
|
|
private int getRenderingIntent (ICC_Profile profile) {
|
|
byte[] header = profile.getData(ICC_Profile.icSigHead);
|
|
int index = ICC_Profile.icHdrRenderingIntent;
|
|
|
|
/* According to ICC spec, only the least-significant 16 bits shall be
|
|
* used to encode the rendering intent. The most significant 16 bits
|
|
* shall be set to zero. Thus, we are ignoring two most significant
|
|
* bytes here.
|
|
*
|
|
* See http://www.color.org/ICC1v42_2006-05.pdf, section 7.2.15.
|
|
*/
|
|
return ((header[index+2] & 0xff) << 8) |
|
|
(header[index+3] & 0xff);
|
|
}
|
|
|
|
/**
|
|
* Returns the rendering hints used by this op.
|
|
* @return the <code>RenderingHints</code> object of this
|
|
* <code>ColorConvertOp</code>
|
|
*/
|
|
public final RenderingHints getRenderingHints() {
|
|
return hints;
|
|
}
|
|
|
|
private final BufferedImage nonICCBIFilter(BufferedImage src,
|
|
ColorSpace srcColorSpace,
|
|
BufferedImage dst,
|
|
ColorSpace dstColorSpace) {
|
|
|
|
int w = src.getWidth();
|
|
int h = src.getHeight();
|
|
ICC_ColorSpace ciespace =
|
|
(ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_CIEXYZ);
|
|
if (dst == null) {
|
|
dst = createCompatibleDestImage(src, null);
|
|
dstColorSpace = dst.getColorModel().getColorSpace();
|
|
} else {
|
|
if ((h != dst.getHeight()) || (w != dst.getWidth())) {
|
|
throw new IllegalArgumentException(
|
|
"Width or height of BufferedImages do not match");
|
|
}
|
|
}
|
|
Raster srcRas = src.getRaster();
|
|
WritableRaster dstRas = dst.getRaster();
|
|
ColorModel srcCM = src.getColorModel();
|
|
ColorModel dstCM = dst.getColorModel();
|
|
int srcNumComp = srcCM.getNumColorComponents();
|
|
int dstNumComp = dstCM.getNumColorComponents();
|
|
boolean dstHasAlpha = dstCM.hasAlpha();
|
|
boolean needSrcAlpha = srcCM.hasAlpha() && dstHasAlpha;
|
|
ColorSpace[] list;
|
|
if ((CSList == null) && (profileList.length != 0)) {
|
|
/* possible non-ICC src, some profiles, possible non-ICC dst */
|
|
boolean nonICCSrc, nonICCDst;
|
|
ICC_Profile srcProfile, dstProfile;
|
|
if (!(srcColorSpace instanceof ICC_ColorSpace)) {
|
|
nonICCSrc = true;
|
|
srcProfile = ciespace.getProfile();
|
|
} else {
|
|
nonICCSrc = false;
|
|
srcProfile = ((ICC_ColorSpace) srcColorSpace).getProfile();
|
|
}
|
|
if (!(dstColorSpace instanceof ICC_ColorSpace)) {
|
|
nonICCDst = true;
|
|
dstProfile = ciespace.getProfile();
|
|
} else {
|
|
nonICCDst = false;
|
|
dstProfile = ((ICC_ColorSpace) dstColorSpace).getProfile();
|
|
}
|
|
/* make a new transform if needed */
|
|
if ((thisTransform == null) || (thisSrcProfile != srcProfile) ||
|
|
(thisDestProfile != dstProfile) ) {
|
|
updateBITransform(srcProfile, dstProfile);
|
|
}
|
|
// process per scanline
|
|
float maxNum = 65535.0f; // use 16-bit precision in CMM
|
|
ColorSpace cs;
|
|
int iccSrcNumComp;
|
|
if (nonICCSrc) {
|
|
cs = ciespace;
|
|
iccSrcNumComp = 3;
|
|
} else {
|
|
cs = srcColorSpace;
|
|
iccSrcNumComp = srcNumComp;
|
|
}
|
|
float[] srcMinVal = new float[iccSrcNumComp];
|
|
float[] srcInvDiffMinMax = new float[iccSrcNumComp];
|
|
for (int i = 0; i < srcNumComp; i++) {
|
|
srcMinVal[i] = cs.getMinValue(i);
|
|
srcInvDiffMinMax[i] = maxNum / (cs.getMaxValue(i) - srcMinVal[i]);
|
|
}
|
|
int iccDstNumComp;
|
|
if (nonICCDst) {
|
|
cs = ciespace;
|
|
iccDstNumComp = 3;
|
|
} else {
|
|
cs = dstColorSpace;
|
|
iccDstNumComp = dstNumComp;
|
|
}
|
|
float[] dstMinVal = new float[iccDstNumComp];
|
|
float[] dstDiffMinMax = new float[iccDstNumComp];
|
|
for (int i = 0; i < dstNumComp; i++) {
|
|
dstMinVal[i] = cs.getMinValue(i);
|
|
dstDiffMinMax[i] = (cs.getMaxValue(i) - dstMinVal[i]) / maxNum;
|
|
}
|
|
float[] dstColor;
|
|
if (dstHasAlpha) {
|
|
int size = ((dstNumComp + 1) > 3) ? (dstNumComp + 1) : 3;
|
|
dstColor = new float[size];
|
|
} else {
|
|
int size = (dstNumComp > 3) ? dstNumComp : 3;
|
|
dstColor = new float[size];
|
|
}
|
|
short[] srcLine = new short[w * iccSrcNumComp];
|
|
short[] dstLine = new short[w * iccDstNumComp];
|
|
Object pixel;
|
|
float[] color;
|
|
float[] alpha = null;
|
|
if (needSrcAlpha) {
|
|
alpha = new float[w];
|
|
}
|
|
int idx;
|
|
// process each scanline
|
|
for (int y = 0; y < h; y++) {
|
|
// convert src scanline
|
|
pixel = null;
|
|
color = null;
|
|
idx = 0;
|
|
for (int x = 0; x < w; x++) {
|
|
pixel = srcRas.getDataElements(x, y, pixel);
|
|
color = srcCM.getNormalizedComponents(pixel, color, 0);
|
|
if (needSrcAlpha) {
|
|
alpha[x] = color[srcNumComp];
|
|
}
|
|
if (nonICCSrc) {
|
|
color = srcColorSpace.toCIEXYZ(color);
|
|
}
|
|
for (int i = 0; i < iccSrcNumComp; i++) {
|
|
srcLine[idx++] = (short)
|
|
((color[i] - srcMinVal[i]) * srcInvDiffMinMax[i] +
|
|
0.5f);
|
|
}
|
|
}
|
|
// color convert srcLine to dstLine
|
|
thisTransform.colorConvert(srcLine, dstLine);
|
|
// convert dst scanline
|
|
pixel = null;
|
|
idx = 0;
|
|
for (int x = 0; x < w; x++) {
|
|
for (int i = 0; i < iccDstNumComp; i++) {
|
|
dstColor[i] = ((float) (dstLine[idx++] & 0xffff)) *
|
|
dstDiffMinMax[i] + dstMinVal[i];
|
|
}
|
|
if (nonICCDst) {
|
|
color = srcColorSpace.fromCIEXYZ(dstColor);
|
|
for (int i = 0; i < dstNumComp; i++) {
|
|
dstColor[i] = color[i];
|
|
}
|
|
}
|
|
if (needSrcAlpha) {
|
|
dstColor[dstNumComp] = alpha[x];
|
|
} else if (dstHasAlpha) {
|
|
dstColor[dstNumComp] = 1.0f;
|
|
}
|
|
pixel = dstCM.getDataElements(dstColor, 0, pixel);
|
|
dstRas.setDataElements(x, y, pixel);
|
|
}
|
|
}
|
|
} else {
|
|
/* possible non-ICC src, possible CSList, possible non-ICC dst */
|
|
// process per pixel
|
|
int numCS;
|
|
if (CSList == null) {
|
|
numCS = 0;
|
|
} else {
|
|
numCS = CSList.length;
|
|
}
|
|
float[] dstColor;
|
|
if (dstHasAlpha) {
|
|
dstColor = new float[dstNumComp + 1];
|
|
} else {
|
|
dstColor = new float[dstNumComp];
|
|
}
|
|
Object spixel = null;
|
|
Object dpixel = null;
|
|
float[] color = null;
|
|
float[] tmpColor;
|
|
// process each pixel
|
|
for (int y = 0; y < h; y++) {
|
|
for (int x = 0; x < w; x++) {
|
|
spixel = srcRas.getDataElements(x, y, spixel);
|
|
color = srcCM.getNormalizedComponents(spixel, color, 0);
|
|
tmpColor = srcColorSpace.toCIEXYZ(color);
|
|
for (int i = 0; i < numCS; i++) {
|
|
tmpColor = CSList[i].fromCIEXYZ(tmpColor);
|
|
tmpColor = CSList[i].toCIEXYZ(tmpColor);
|
|
}
|
|
tmpColor = dstColorSpace.fromCIEXYZ(tmpColor);
|
|
for (int i = 0; i < dstNumComp; i++) {
|
|
dstColor[i] = tmpColor[i];
|
|
}
|
|
if (needSrcAlpha) {
|
|
dstColor[dstNumComp] = color[srcNumComp];
|
|
} else if (dstHasAlpha) {
|
|
dstColor[dstNumComp] = 1.0f;
|
|
}
|
|
dpixel = dstCM.getDataElements(dstColor, 0, dpixel);
|
|
dstRas.setDataElements(x, y, dpixel);
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
return dst;
|
|
}
|
|
|
|
/* color convert a Raster - handles byte, ushort, int, short, float,
|
|
or double transferTypes */
|
|
private final WritableRaster nonICCRasterFilter(Raster src,
|
|
WritableRaster dst) {
|
|
|
|
if (CSList.length != 2) {
|
|
throw new IllegalArgumentException(
|
|
"Destination ColorSpace is undefined");
|
|
}
|
|
if (src.getNumBands() != CSList[0].getNumComponents()) {
|
|
throw new IllegalArgumentException(
|
|
"Numbers of source Raster bands and source color space " +
|
|
"components do not match");
|
|
}
|
|
if (dst == null) {
|
|
dst = createCompatibleDestRaster(src);
|
|
} else {
|
|
if (src.getHeight() != dst.getHeight() ||
|
|
src.getWidth() != dst.getWidth()) {
|
|
throw new IllegalArgumentException(
|
|
"Width or height of Rasters do not match");
|
|
}
|
|
if (dst.getNumBands() != CSList[1].getNumComponents()) {
|
|
throw new IllegalArgumentException(
|
|
"Numbers of destination Raster bands and destination " +
|
|
"color space components do not match");
|
|
}
|
|
}
|
|
|
|
if (srcMinVals == null) {
|
|
getMinMaxValsFromColorSpaces(CSList[0], CSList[1]);
|
|
}
|
|
|
|
SampleModel srcSM = src.getSampleModel();
|
|
SampleModel dstSM = dst.getSampleModel();
|
|
boolean srcIsFloat, dstIsFloat;
|
|
int srcTransferType = src.getTransferType();
|
|
int dstTransferType = dst.getTransferType();
|
|
if ((srcTransferType == DataBuffer.TYPE_FLOAT) ||
|
|
(srcTransferType == DataBuffer.TYPE_DOUBLE)) {
|
|
srcIsFloat = true;
|
|
} else {
|
|
srcIsFloat = false;
|
|
}
|
|
if ((dstTransferType == DataBuffer.TYPE_FLOAT) ||
|
|
(dstTransferType == DataBuffer.TYPE_DOUBLE)) {
|
|
dstIsFloat = true;
|
|
} else {
|
|
dstIsFloat = false;
|
|
}
|
|
int w = src.getWidth();
|
|
int h = src.getHeight();
|
|
int srcNumBands = src.getNumBands();
|
|
int dstNumBands = dst.getNumBands();
|
|
float[] srcScaleFactor = null;
|
|
float[] dstScaleFactor = null;
|
|
if (!srcIsFloat) {
|
|
srcScaleFactor = new float[srcNumBands];
|
|
for (int i = 0; i < srcNumBands; i++) {
|
|
if (srcTransferType == DataBuffer.TYPE_SHORT) {
|
|
srcScaleFactor[i] = (srcMaxVals[i] - srcMinVals[i]) /
|
|
32767.0f;
|
|
} else {
|
|
srcScaleFactor[i] = (srcMaxVals[i] - srcMinVals[i]) /
|
|
((float) ((1 << srcSM.getSampleSize(i)) - 1));
|
|
}
|
|
}
|
|
}
|
|
if (!dstIsFloat) {
|
|
dstScaleFactor = new float[dstNumBands];
|
|
for (int i = 0; i < dstNumBands; i++) {
|
|
if (dstTransferType == DataBuffer.TYPE_SHORT) {
|
|
dstScaleFactor[i] = 32767.0f /
|
|
(dstMaxVals[i] - dstMinVals[i]);
|
|
} else {
|
|
dstScaleFactor[i] =
|
|
((float) ((1 << dstSM.getSampleSize(i)) - 1)) /
|
|
(dstMaxVals[i] - dstMinVals[i]);
|
|
}
|
|
}
|
|
}
|
|
int ys = src.getMinY();
|
|
int yd = dst.getMinY();
|
|
int xs, xd;
|
|
float sample;
|
|
float[] color = new float[srcNumBands];
|
|
float[] tmpColor;
|
|
ColorSpace srcColorSpace = CSList[0];
|
|
ColorSpace dstColorSpace = CSList[1];
|
|
// process each pixel
|
|
for (int y = 0; y < h; y++, ys++, yd++) {
|
|
// get src scanline
|
|
xs = src.getMinX();
|
|
xd = dst.getMinX();
|
|
for (int x = 0; x < w; x++, xs++, xd++) {
|
|
for (int i = 0; i < srcNumBands; i++) {
|
|
sample = src.getSampleFloat(xs, ys, i);
|
|
if (!srcIsFloat) {
|
|
sample = sample * srcScaleFactor[i] + srcMinVals[i];
|
|
}
|
|
color[i] = sample;
|
|
}
|
|
tmpColor = srcColorSpace.toCIEXYZ(color);
|
|
tmpColor = dstColorSpace.fromCIEXYZ(tmpColor);
|
|
for (int i = 0; i < dstNumBands; i++) {
|
|
sample = tmpColor[i];
|
|
if (!dstIsFloat) {
|
|
sample = (sample - dstMinVals[i]) * dstScaleFactor[i];
|
|
}
|
|
dst.setSample(xd, yd, i, sample);
|
|
}
|
|
}
|
|
}
|
|
return dst;
|
|
}
|
|
|
|
private void getMinMaxValsFromProfiles(ICC_Profile srcProfile,
|
|
ICC_Profile dstProfile) {
|
|
int type = srcProfile.getColorSpaceType();
|
|
int nc = srcProfile.getNumComponents();
|
|
srcMinVals = new float[nc];
|
|
srcMaxVals = new float[nc];
|
|
setMinMax(type, nc, srcMinVals, srcMaxVals);
|
|
type = dstProfile.getColorSpaceType();
|
|
nc = dstProfile.getNumComponents();
|
|
dstMinVals = new float[nc];
|
|
dstMaxVals = new float[nc];
|
|
setMinMax(type, nc, dstMinVals, dstMaxVals);
|
|
}
|
|
|
|
private void setMinMax(int type, int nc, float[] minVals, float[] maxVals) {
|
|
if (type == ColorSpace.TYPE_Lab) {
|
|
minVals[0] = 0.0f; // L
|
|
maxVals[0] = 100.0f;
|
|
minVals[1] = -128.0f; // a
|
|
maxVals[1] = 127.0f;
|
|
minVals[2] = -128.0f; // b
|
|
maxVals[2] = 127.0f;
|
|
} else if (type == ColorSpace.TYPE_XYZ) {
|
|
minVals[0] = minVals[1] = minVals[2] = 0.0f; // X, Y, Z
|
|
maxVals[0] = maxVals[1] = maxVals[2] = 1.0f + (32767.0f/ 32768.0f);
|
|
} else {
|
|
for (int i = 0; i < nc; i++) {
|
|
minVals[i] = 0.0f;
|
|
maxVals[i] = 1.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void getMinMaxValsFromColorSpaces(ColorSpace srcCspace,
|
|
ColorSpace dstCspace) {
|
|
int nc = srcCspace.getNumComponents();
|
|
srcMinVals = new float[nc];
|
|
srcMaxVals = new float[nc];
|
|
for (int i = 0; i < nc; i++) {
|
|
srcMinVals[i] = srcCspace.getMinValue(i);
|
|
srcMaxVals[i] = srcCspace.getMaxValue(i);
|
|
}
|
|
nc = dstCspace.getNumComponents();
|
|
dstMinVals = new float[nc];
|
|
dstMaxVals = new float[nc];
|
|
for (int i = 0; i < nc; i++) {
|
|
dstMinVals[i] = dstCspace.getMinValue(i);
|
|
dstMaxVals[i] = dstCspace.getMaxValue(i);
|
|
}
|
|
}
|
|
|
|
}
|