Frames | No Frames |
1: /* ColorConvertOp.java -- 2: Copyright (C) 2004, 2006 Free Software Foundation 3: 4: This file is part of GNU Classpath. 5: 6: GNU Classpath is free software; you can redistribute it and/or modify 7: it under the terms of the GNU General Public License as published by 8: the Free Software Foundation; either version 2, or (at your option) 9: any later version. 10: 11: GNU Classpath is distributed in the hope that it will be useful, but 12: WITHOUT ANY WARRANTY; without even the implied warranty of 13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14: General Public License for more details. 15: 16: You should have received a copy of the GNU General Public License 17: along with GNU Classpath; see the file COPYING. If not, write to the 18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 19: 02110-1301 USA. 20: 21: Linking this library statically or dynamically with other modules is 22: making a combined work based on this library. Thus, the terms and 23: conditions of the GNU General Public License cover the whole 24: combination. 25: 26: As a special exception, the copyright holders of this library give you 27: permission to link this library with independent modules to produce an 28: executable, regardless of the license terms of these independent 29: modules, and to copy and distribute the resulting executable under 30: terms of your choice, provided that you also meet, for each linked 31: independent module, the terms and conditions of the license of that 32: module. An independent module is a module which is not derived from 33: or based on this library. If you modify this library, you may extend 34: this exception to your version of the library, but you are not 35: obligated to do so. If you do not wish to do so, delete this 36: exception statement from your version. */ 37: 38: 39: package java.awt.image; 40: 41: import gnu.java.awt.Buffers; 42: 43: import java.awt.Graphics2D; 44: import java.awt.Point; 45: import java.awt.RenderingHints; 46: import java.awt.color.ColorSpace; 47: import java.awt.color.ICC_ColorSpace; 48: import java.awt.color.ICC_Profile; 49: import java.awt.geom.Point2D; 50: import java.awt.geom.Rectangle2D; 51: 52: /** 53: * ColorConvertOp is a filter for converting images or rasters between 54: * colorspaces, either through a sequence of colorspaces or just from source to 55: * destination. 56: * 57: * Color conversion is done on the color components without alpha. Thus 58: * if a BufferedImage has alpha premultiplied, this is divided out before 59: * color conversion, and premultiplication applied if the destination 60: * requires it. 61: * 62: * Color rendering and dithering hints may be applied if specified. This is 63: * likely platform-dependent. 64: * 65: * @author jlquinn@optonline.net 66: */ 67: public class ColorConvertOp implements BufferedImageOp, RasterOp 68: { 69: private RenderingHints hints; 70: private ICC_Profile[] profiles = null; 71: private ColorSpace[] spaces; 72: 73: 74: /** 75: * Convert a BufferedImage through a ColorSpace. 76: * 77: * Objects created with this constructor can be used to convert 78: * BufferedImage's to a destination ColorSpace. Attempts to convert Rasters 79: * with this constructor will result in an IllegalArgumentException when the 80: * filter(Raster, WritableRaster) method is called. 81: * 82: * @param cspace The target color space. 83: * @param hints Rendering hints to use in conversion, if any (may be null) 84: * @throws NullPointerException if the ColorSpace is null. 85: */ 86: public ColorConvertOp(ColorSpace cspace, RenderingHints hints) 87: { 88: if (cspace == null) 89: throw new NullPointerException(); 90: spaces = new ColorSpace[]{cspace}; 91: this.hints = hints; 92: } 93: 94: /** 95: * Convert from a source colorspace to a destination colorspace. 96: * 97: * This constructor takes two ColorSpace arguments as the source and 98: * destination color spaces. It is usually used with the 99: * filter(Raster, WritableRaster) method, in which case the source colorspace 100: * is assumed to correspond to the source Raster, and the destination 101: * colorspace with the destination Raster. 102: * 103: * If used with BufferedImages that do not match the source or destination 104: * colorspaces specified here, there is an implicit conversion from the 105: * source image to the source ColorSpace, or the destination ColorSpace to 106: * the destination image. 107: * 108: * @param srcCspace The source ColorSpace. 109: * @param dstCspace The destination ColorSpace. 110: * @param hints Rendering hints to use in conversion, if any (may be null). 111: * @throws NullPointerException if any ColorSpace is null. 112: */ 113: public ColorConvertOp(ColorSpace srcCspace, ColorSpace dstCspace, 114: RenderingHints hints) 115: { 116: if (srcCspace == null || dstCspace == null) 117: throw new NullPointerException(); 118: spaces = new ColorSpace[]{srcCspace, dstCspace}; 119: this.hints = hints; 120: } 121: 122: /** 123: * Convert from a source colorspace to a destinatino colorspace. 124: * 125: * This constructor builds a ColorConvertOp from an array of ICC_Profiles. 126: * The source will be converted through the sequence of color spaces 127: * defined by the profiles. If the sequence of profiles doesn't give a 128: * well-defined conversion, an IllegalArgumentException is thrown. 129: * 130: * If used with BufferedImages that do not match the source or destination 131: * colorspaces specified here, there is an implicit conversion from the 132: * source image to the source ColorSpace, or the destination ColorSpace to 133: * the destination image. 134: * 135: * For Rasters, the first and last profiles must have the same number of 136: * bands as the source and destination Rasters, respectively. If this is 137: * not the case, or there fewer than 2 profiles, an IllegalArgumentException 138: * will be thrown. 139: * 140: * @param profiles An array of ICC_Profile's to convert through. 141: * @param hints Rendering hints to use in conversion, if any (may be null). 142: * @throws NullPointerException if the profile array is null. 143: * @throws IllegalArgumentException if the array is not a well-defined 144: * conversion. 145: */ 146: public ColorConvertOp(ICC_Profile[] profiles, RenderingHints hints) 147: { 148: if (profiles == null) 149: throw new NullPointerException(); 150: 151: this.hints = hints; 152: this.profiles = profiles; 153: 154: // Create colorspace array with space for src and dest colorspace 155: // Note that the ICC_ColorSpace constructor will throw an 156: // IllegalArgumentException if the profile is invalid; thus we check 157: // for a "well defined conversion" 158: spaces = new ColorSpace[profiles.length]; 159: for (int i = 0; i < profiles.length; i++) 160: spaces[i] = new ICC_ColorSpace(profiles[i]); 161: } 162: 163: /** 164: * Convert from source color space to destination color space. 165: * 166: * Only valid for BufferedImage objects, this Op converts from the source 167: * image's color space to the destination image's color space. 168: * 169: * The destination in the filter(BufferedImage, BufferedImage) method cannot 170: * be null for this operation, and it also cannot be used with the 171: * filter(Raster, WritableRaster) method. 172: * 173: * @param hints Rendering hints to use in conversion, if any (may be null). 174: */ 175: public ColorConvertOp(RenderingHints hints) 176: { 177: this.hints = hints; 178: spaces = new ColorSpace[0]; 179: } 180: 181: /** 182: * Converts the source image using the conversion path specified in the 183: * constructor. The resulting image is stored in the destination image if one 184: * is provided; otherwise a new BufferedImage is created and returned. 185: * 186: * The source and destination BufferedImage (if one is supplied) must have 187: * the same dimensions. 188: * 189: * @param src The source image. 190: * @param dst The destination image. 191: * @throws IllegalArgumentException if the rasters and/or color spaces are 192: * incompatible. 193: * @return The transformed image. 194: */ 195: public final BufferedImage filter(BufferedImage src, BufferedImage dst) 196: { 197: // TODO: The plan is to create a scanline buffer for intermediate buffers. 198: // For now we just suck it up and create intermediate buffers. 199: 200: if (dst == null && spaces.length == 0) 201: throw new IllegalArgumentException("Not enough color space information " 202: + "to complete conversion."); 203: 204: if (dst != null 205: && (src.getHeight() != dst.getHeight() || src.getWidth() != dst.getWidth())) 206: throw new IllegalArgumentException("Source and destination images have " 207: + "different dimensions"); 208: 209: // Make sure input isn't premultiplied by alpha 210: if (src.isAlphaPremultiplied()) 211: { 212: BufferedImage tmp = createCompatibleDestImage(src, src.getColorModel()); 213: copyimage(src, tmp); 214: tmp.coerceData(false); 215: src = tmp; 216: } 217: 218: // Convert through defined intermediate conversions 219: BufferedImage tmp; 220: for (int i = 0; i < spaces.length; i++) 221: { 222: if (src.getColorModel().getColorSpace().getType() != spaces[i].getType()) 223: { 224: tmp = createCompatibleDestImage(src, 225: createCompatibleColorModel(src, 226: spaces[i])); 227: copyimage(src, tmp); 228: src = tmp; 229: } 230: } 231: 232: // No implicit conversion to destination type needed; return result from the 233: // last intermediate conversions (which was left in src) 234: if (dst == null) 235: dst = src; 236: 237: // Implicit conversion to destination image's color space 238: else 239: copyimage(src, dst); 240: 241: return dst; 242: } 243: 244: /** 245: * Converts the source raster using the conversion path specified in the 246: * constructor. The resulting raster is stored in the destination raster if 247: * one is provided; otherwise a new WritableRaster is created and returned. 248: * 249: * This operation is not valid with every constructor of this class; see 250: * the constructors for details. Further, the source raster must have the 251: * same number of bands as the source ColorSpace, and the destination raster 252: * must have the same number of bands as the destination ColorSpace. 253: * 254: * The source and destination raster (if one is supplied) must also have the 255: * same dimensions. 256: * 257: * @param src The source raster. 258: * @param dest The destination raster. 259: * @throws IllegalArgumentException if the rasters and/or color spaces are 260: * incompatible. 261: * @return The transformed raster. 262: */ 263: public final WritableRaster filter(Raster src, WritableRaster dest) 264: { 265: // Various checks to ensure that the rasters and color spaces are compatible 266: if (spaces.length < 2) 267: throw new IllegalArgumentException("Not enough information about " + 268: "source and destination colorspaces."); 269: 270: if (spaces[0].getNumComponents() != src.getNumBands() 271: || (dest != null && spaces[spaces.length - 1].getNumComponents() != dest.getNumBands())) 272: throw new IllegalArgumentException("Source or destination raster " + 273: "contains the wrong number of bands."); 274: 275: if (dest != null 276: && (src.getHeight() != dest.getHeight() || src.getWidth() != dest.getWidth())) 277: throw new IllegalArgumentException("Source and destination rasters " + 278: "have different dimensions"); 279: 280: // Need to iterate through each color space. 281: // spaces[0] corresponds to the ColorSpace of the source raster, and 282: // spaces[spaces.length - 1] corresponds to the ColorSpace of the 283: // destination, with any number (or zero) of intermediate conversions. 284: 285: for (int i = 0; i < spaces.length - 2; i++) 286: { 287: WritableRaster tmp = createCompatibleDestRaster(src, spaces[i + 1], 288: false, 289: src.getTransferType()); 290: copyraster(src, spaces[i], tmp, spaces[i + 1]); 291: src = tmp; 292: } 293: 294: // The last conversion is done outside of the loop so that we can 295: // use the dest raster supplied, instead of creating our own temp raster 296: if (dest == null) 297: dest = createCompatibleDestRaster(src, spaces[spaces.length - 1], false, 298: DataBuffer.TYPE_BYTE); 299: copyraster(src, spaces[spaces.length - 2], dest, spaces[spaces.length - 1]); 300: 301: return dest; 302: } 303: 304: /** 305: * Creates an empty BufferedImage with the size equal to the source and the 306: * correct number of bands for the conversion defined in this Op. The newly 307: * created image is created with the specified ColorModel, or if no ColorModel 308: * is supplied, an appropriate one is chosen. 309: * 310: * @param src The source image. 311: * @param dstCM A color model for the destination image (may be null). 312: * @throws IllegalArgumentException if an appropriate colormodel cannot be 313: * chosen with the information given. 314: * @return The new compatible destination image. 315: */ 316: public BufferedImage createCompatibleDestImage(BufferedImage src, 317: ColorModel dstCM) 318: { 319: if (dstCM == null && spaces.length == 0) 320: throw new IllegalArgumentException("Don't know the destination " + 321: "colormodel"); 322: 323: if (dstCM == null) 324: { 325: dstCM = createCompatibleColorModel(src, spaces[spaces.length - 1]); 326: } 327: 328: return new BufferedImage(dstCM, 329: createCompatibleDestRaster(src.getRaster(), 330: dstCM.getColorSpace(), 331: src.getColorModel().hasAlpha, 332: dstCM.getTransferType()), 333: src.isPremultiplied, null); 334: } 335: 336: /** 337: * Creates a new WritableRaster with the size equal to the source and the 338: * correct number of bands. 339: * 340: * Note, the new Raster will always use a BYTE storage size, regardless of 341: * the color model or defined destination; this is for compatibility with 342: * the reference implementation. 343: * 344: * @param src The source Raster. 345: * @throws IllegalArgumentException if there isn't enough colorspace 346: * information to create a compatible Raster. 347: * @return The new compatible destination raster. 348: */ 349: public WritableRaster createCompatibleDestRaster(Raster src) 350: { 351: if (spaces.length < 2) 352: throw new IllegalArgumentException("Not enough destination colorspace " + 353: "information"); 354: 355: // Create a new raster with the last ColorSpace in the conversion 356: // chain, and with no alpha (implied) 357: return createCompatibleDestRaster(src, spaces[spaces.length-1], false, 358: DataBuffer.TYPE_BYTE); 359: } 360: 361: /** 362: * Returns the array of ICC_Profiles used to create this Op, or null if the 363: * Op was created using ColorSpace arguments. 364: * 365: * @return The array of ICC_Profiles, or null. 366: */ 367: public final ICC_Profile[] getICC_Profiles() 368: { 369: return profiles; 370: } 371: 372: /** 373: * Returns the rendering hints for this op. 374: * 375: * @return The rendering hints for this Op, or null. 376: */ 377: public final RenderingHints getRenderingHints() 378: { 379: return hints; 380: } 381: 382: /** 383: * Returns the corresponding destination point for a source point. 384: * Because this is not a geometric operation, the destination and source 385: * points will be identical. 386: * 387: * @param src The source point. 388: * @param dst The transformed destination point. 389: * @return The transformed destination point. 390: */ 391: public final Point2D getPoint2D(Point2D src, Point2D dst) 392: { 393: if (dst == null) 394: return (Point2D)src.clone(); 395: 396: dst.setLocation(src); 397: return dst; 398: } 399: 400: /** 401: * Returns the corresponding destination boundary of a source boundary. 402: * Because this is not a geometric operation, the destination and source 403: * boundaries will be identical. 404: * 405: * @param src The source boundary. 406: * @return The boundaries of the destination. 407: */ 408: public final Rectangle2D getBounds2D(BufferedImage src) 409: { 410: return src.getRaster().getBounds(); 411: } 412: 413: /** 414: * Returns the corresponding destination boundary of a source boundary. 415: * Because this is not a geometric operation, the destination and source 416: * boundaries will be identical. 417: * 418: * @param src The source boundary. 419: * @return The boundaries of the destination. 420: */ 421: public final Rectangle2D getBounds2D(Raster src) 422: { 423: return src.getBounds(); 424: } 425: 426: /** 427: * Copy a source image to a destination image, respecting their colorspaces 428: * and performing colorspace conversions if necessary. 429: * 430: * @param src The source image. 431: * @param dst The destination image. 432: */ 433: private void copyimage(BufferedImage src, BufferedImage dst) 434: { 435: // This is done using Graphics2D in order to respect the rendering hints. 436: Graphics2D gg = dst.createGraphics(); 437: 438: // If no hints are set there is no need to call 439: // setRenderingHints on the Graphics2D object. 440: if (hints != null) 441: gg.setRenderingHints(hints); 442: 443: gg.drawImage(src, 0, 0, null); 444: gg.dispose(); 445: } 446: 447: /** 448: * Copy a source raster to a destination raster, performing a colorspace 449: * conversion between the two. The conversion will respect the 450: * KEY_COLOR_RENDERING rendering hint if one is present. 451: * 452: * @param src The source raster. 453: * @param scs The colorspace of the source raster. 454: * @dst The destination raster. 455: * @dcs The colorspace of the destination raster. 456: */ 457: private void copyraster(Raster src, ColorSpace scs, WritableRaster dst, ColorSpace dcs) 458: { 459: float[] sbuf = new float[src.getNumBands()]; 460: 461: if (hints != null 462: && hints.get(RenderingHints.KEY_COLOR_RENDERING) == 463: RenderingHints.VALUE_COLOR_RENDER_QUALITY) 464: { 465: // use cie for accuracy 466: for (int y = src.getMinY(); y < src.getHeight() + src.getMinY(); y++) 467: for (int x = src.getMinX(); x < src.getWidth() + src.getMinX(); x++) 468: dst.setPixel(x, y, 469: dcs.fromCIEXYZ(scs.toCIEXYZ(src.getPixel(x, y, sbuf)))); 470: } 471: else 472: { 473: // use rgb - it's probably faster 474: for (int y = src.getMinY(); y < src.getHeight() + src.getMinY(); y++) 475: for (int x = src.getMinX(); x < src.getWidth() + src.getMinX(); x++) 476: dst.setPixel(x, y, 477: dcs.fromRGB(scs.toRGB(src.getPixel(x, y, sbuf)))); 478: } 479: } 480: 481: /** 482: * This method creates a color model with the same colorspace and alpha 483: * settings as the source image. The created color model will always be a 484: * ComponentColorModel and have a BYTE transfer type. 485: * 486: * @param img The source image. 487: * @param cs The ColorSpace to use. 488: * @return A color model compatible with the source image. 489: */ 490: private ColorModel createCompatibleColorModel(BufferedImage img, ColorSpace cs) 491: { 492: // The choice of ComponentColorModel and DataBuffer.TYPE_BYTE is based on 493: // Mauve testing of the reference implementation. 494: return new ComponentColorModel(cs, 495: img.getColorModel().hasAlpha(), 496: img.isAlphaPremultiplied(), 497: img.getColorModel().getTransparency(), 498: DataBuffer.TYPE_BYTE); 499: } 500: 501: /** 502: * This method creates a compatible Raster, given a source raster, colorspace, 503: * alpha value, and transfer type. 504: * 505: * @param src The source raster. 506: * @param cs The ColorSpace to use. 507: * @param hasAlpha Whether the raster should include a component for an alpha. 508: * @param transferType The size of a single data element. 509: * @return A compatible WritableRaster. 510: */ 511: private WritableRaster createCompatibleDestRaster(Raster src, ColorSpace cs, 512: boolean hasAlpha, 513: int transferType) 514: { 515: // The use of a PixelInterleavedSampleModel weas determined using mauve 516: // tests, based on the reference implementation 517: 518: int numComponents = cs.getNumComponents(); 519: if (hasAlpha) 520: numComponents++; 521: 522: int[] offsets = new int[numComponents]; 523: for (int i = 0; i < offsets.length; i++) 524: offsets[i] = i; 525: 526: DataBuffer db = Buffers.createBuffer(transferType, 527: src.getWidth() * src.getHeight() * numComponents, 528: 1); 529: return new WritableRaster(new PixelInterleavedSampleModel(transferType, 530: src.getWidth(), 531: src.getHeight(), 532: numComponents, 533: numComponents * src.getWidth(), 534: offsets), 535: db, new Point(src.getMinX(), src.getMinY())); 536: } 537: }