Source for java.awt.image.ColorConvertOp

   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: }