/* * Copyright (c) 1997, 2008, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ /********************************************************************** ********************************************************************** ********************************************************************** *** 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. * <p> * @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); } } }