1 /* 2 * Copyright (c) 1997, 2005, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package java.awt.image; 27 28 import java.awt.geom.AffineTransform; 29 import java.awt.geom.NoninvertibleTransformException; 30 import java.awt.geom.Rectangle2D; 31 import java.awt.geom.Point2D; 32 import java.awt.AlphaComposite; 33 import java.awt.GraphicsEnvironment; 34 import java.awt.Rectangle; 35 import java.awt.RenderingHints; 36 import java.awt.Transparency; 37 import javax.tools.annotation.GenerateNativeHeader; 38 import sun.awt.image.ImagingLib; 39 40 /** 41 * This class uses an affine transform to perform a linear mapping from 42 * 2D coordinates in the source image or <CODE>Raster</CODE> to 2D coordinates 43 * in the destination image or <CODE>Raster</CODE>. 44 * The type of interpolation that is used is specified through a constructor, 45 * either by a <CODE>RenderingHints</CODE> object or by one of the integer 46 * interpolation types defined in this class. 47 * <p> 48 * If a <CODE>RenderingHints</CODE> object is specified in the constructor, the 49 * interpolation hint and the rendering quality hint are used to set 50 * the interpolation type for this operation. The color rendering hint 51 * and the dithering hint can be used when color conversion is required. 52 * <p> 53 * Note that the following constraints have to be met: 54 * <ul> 55 * <li>The source and destination must be different. 56 * <li>For <CODE>Raster</CODE> objects, the number of bands in the source must 57 * be equal to the number of bands in the destination. 58 * </ul> 59 * @see AffineTransform 60 * @see BufferedImageFilter 61 * @see java.awt.RenderingHints#KEY_INTERPOLATION 62 * @see java.awt.RenderingHints#KEY_RENDERING 63 * @see java.awt.RenderingHints#KEY_COLOR_RENDERING 64 * @see java.awt.RenderingHints#KEY_DITHERING 65 */ 66 /* No native methods here, but the constants are needed in the supporting JNI code */ 67 @GenerateNativeHeader 68 public class AffineTransformOp implements BufferedImageOp, RasterOp { 69 private AffineTransform xform; 70 RenderingHints hints; 71 72 /** 73 * Nearest-neighbor interpolation type. 74 */ 75 public static final int TYPE_NEAREST_NEIGHBOR = 1; 76 77 /** 78 * Bilinear interpolation type. 79 */ 80 public static final int TYPE_BILINEAR = 2; 81 82 /** 83 * Bicubic interpolation type. 84 */ 85 public static final int TYPE_BICUBIC = 3; 86 87 int interpolationType = TYPE_NEAREST_NEIGHBOR; 88 89 /** 90 * Constructs an <CODE>AffineTransformOp</CODE> given an affine transform. 91 * The interpolation type is determined from the 92 * <CODE>RenderingHints</CODE> object. If the interpolation hint is 93 * defined, it will be used. Otherwise, if the rendering quality hint is 94 * defined, the interpolation type is determined from its value. If no 95 * hints are specified (<CODE>hints</CODE> is null), 96 * the interpolation type is {@link #TYPE_NEAREST_NEIGHBOR 97 * TYPE_NEAREST_NEIGHBOR}. 98 * 99 * @param xform The <CODE>AffineTransform</CODE> to use for the 100 * operation. 101 * 102 * @param hints The <CODE>RenderingHints</CODE> object used to specify 103 * the interpolation type for the operation. 104 * 105 * @throws ImagingOpException if the transform is non-invertible. 106 * @see java.awt.RenderingHints#KEY_INTERPOLATION 107 * @see java.awt.RenderingHints#KEY_RENDERING 108 */ 109 public AffineTransformOp(AffineTransform xform, RenderingHints hints){ 110 validateTransform(xform); 111 this.xform = (AffineTransform) xform.clone(); 112 this.hints = hints; 113 114 if (hints != null) { 115 Object value = hints.get(hints.KEY_INTERPOLATION); 116 if (value == null) { 117 value = hints.get(hints.KEY_RENDERING); 118 if (value == hints.VALUE_RENDER_SPEED) { 119 interpolationType = TYPE_NEAREST_NEIGHBOR; 120 } 121 else if (value == hints.VALUE_RENDER_QUALITY) { 122 interpolationType = TYPE_BILINEAR; 123 } 124 } 125 else if (value == hints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR) { 126 interpolationType = TYPE_NEAREST_NEIGHBOR; 127 } 128 else if (value == hints.VALUE_INTERPOLATION_BILINEAR) { 129 interpolationType = TYPE_BILINEAR; 130 } 131 else if (value == hints.VALUE_INTERPOLATION_BICUBIC) { 132 interpolationType = TYPE_BICUBIC; 133 } 134 } 135 else { 136 interpolationType = TYPE_NEAREST_NEIGHBOR; 137 } 138 } 139 140 /** 141 * Constructs an <CODE>AffineTransformOp</CODE> given an affine transform 142 * and the interpolation type. 143 * 144 * @param xform The <CODE>AffineTransform</CODE> to use for the operation. 145 * @param interpolationType One of the integer 146 * interpolation type constants defined by this class: 147 * {@link #TYPE_NEAREST_NEIGHBOR TYPE_NEAREST_NEIGHBOR}, 148 * {@link #TYPE_BILINEAR TYPE_BILINEAR}, 149 * {@link #TYPE_BICUBIC TYPE_BICUBIC}. 150 * @throws ImagingOpException if the transform is non-invertible. 151 */ 152 public AffineTransformOp(AffineTransform xform, int interpolationType) { 153 validateTransform(xform); 154 this.xform = (AffineTransform)xform.clone(); 155 switch(interpolationType) { 156 case TYPE_NEAREST_NEIGHBOR: 157 case TYPE_BILINEAR: 158 case TYPE_BICUBIC: 159 break; 160 default: 161 throw new IllegalArgumentException("Unknown interpolation type: "+ 162 interpolationType); 163 } 164 this.interpolationType = interpolationType; 165 } 166 167 /** 168 * Returns the interpolation type used by this op. 169 * @return the interpolation type. 170 * @see #TYPE_NEAREST_NEIGHBOR 171 * @see #TYPE_BILINEAR 172 * @see #TYPE_BICUBIC 173 */ 174 public final int getInterpolationType() { 175 return interpolationType; 176 } 177 178 /** 179 * Transforms the source <CODE>BufferedImage</CODE> and stores the results 180 * in the destination <CODE>BufferedImage</CODE>. 181 * If the color models for the two images do not match, a color 182 * conversion into the destination color model is performed. 183 * If the destination image is null, 184 * a <CODE>BufferedImage</CODE> is created with the source 185 * <CODE>ColorModel</CODE>. 186 * <p> 187 * The coordinates of the rectangle returned by 188 * <code>getBounds2D(BufferedImage)</code> 189 * are not necessarily the same as the coordinates of the 190 * <code>BufferedImage</code> returned by this method. If the 191 * upper-left corner coordinates of the rectangle are 192 * negative then this part of the rectangle is not drawn. If the 193 * upper-left corner coordinates of the rectangle are positive 194 * then the filtered image is drawn at that position in the 195 * destination <code>BufferedImage</code>. 196 * <p> 197 * An <CODE>IllegalArgumentException</CODE> is thrown if the source is 198 * the same as the destination. 199 * 200 * @param src The <CODE>BufferedImage</CODE> to transform. 201 * @param dst The <CODE>BufferedImage</CODE> in which to store the results 202 * of the transformation. 203 * 204 * @return The filtered <CODE>BufferedImage</CODE>. 205 * @throws IllegalArgumentException if <code>src</code> and 206 * <code>dst</code> are the same 207 * @throws ImagingOpException if the image cannot be transformed 208 * because of a data-processing error that might be 209 * caused by an invalid image format, tile format, or 210 * image-processing operation, or any other unsupported 211 * operation. 212 */ 213 public final BufferedImage filter(BufferedImage src, BufferedImage dst) { 214 215 if (src == null) { 216 throw new NullPointerException("src image is null"); 217 } 218 if (src == dst) { 219 throw new IllegalArgumentException("src image cannot be the "+ 220 "same as the dst image"); 221 } 222 223 boolean needToConvert = false; 224 ColorModel srcCM = src.getColorModel(); 225 ColorModel dstCM; 226 BufferedImage origDst = dst; 227 228 if (dst == null) { 229 dst = createCompatibleDestImage(src, null); 230 dstCM = srcCM; 231 origDst = dst; 232 } 233 else { 234 dstCM = dst.getColorModel(); 235 if (srcCM.getColorSpace().getType() != 236 dstCM.getColorSpace().getType()) 237 { 238 int type = xform.getType(); 239 boolean needTrans = ((type& 240 (xform.TYPE_MASK_ROTATION| 241 xform.TYPE_GENERAL_TRANSFORM)) 242 != 0); 243 if (! needTrans && type != xform.TYPE_TRANSLATION && type != xform.TYPE_IDENTITY) 244 { 245 double[] mtx = new double[4]; 246 xform.getMatrix(mtx); 247 // Check out the matrix. A non-integral scale will force ARGB 248 // since the edge conditions can't be guaranteed. 249 needTrans = (mtx[0] != (int)mtx[0] || mtx[3] != (int)mtx[3]); 250 } 251 252 if (needTrans && 253 srcCM.getTransparency() == Transparency.OPAQUE) 254 { 255 // Need to convert first 256 ColorConvertOp ccop = new ColorConvertOp(hints); 257 BufferedImage tmpSrc = null; 258 int sw = src.getWidth(); 259 int sh = src.getHeight(); 260 if (dstCM.getTransparency() == Transparency.OPAQUE) { 261 tmpSrc = new BufferedImage(sw, sh, 262 BufferedImage.TYPE_INT_ARGB); 263 } 264 else { 265 WritableRaster r = 266 dstCM.createCompatibleWritableRaster(sw, sh); 267 tmpSrc = new BufferedImage(dstCM, r, 268 dstCM.isAlphaPremultiplied(), 269 null); 270 } 271 src = ccop.filter(src, tmpSrc); 272 } 273 else { 274 needToConvert = true; 275 dst = createCompatibleDestImage(src, null); 276 } 277 } 278 279 } 280 281 if (interpolationType != TYPE_NEAREST_NEIGHBOR && 282 dst.getColorModel() instanceof IndexColorModel) { 283 dst = new BufferedImage(dst.getWidth(), dst.getHeight(), 284 BufferedImage.TYPE_INT_ARGB); 285 } 286 if (ImagingLib.filter(this, src, dst) == null) { 287 throw new ImagingOpException ("Unable to transform src image"); 288 } 289 290 if (needToConvert) { 291 ColorConvertOp ccop = new ColorConvertOp(hints); 292 ccop.filter(dst, origDst); 293 } 294 else if (origDst != dst) { 295 java.awt.Graphics2D g = origDst.createGraphics(); 296 try { 297 g.setComposite(AlphaComposite.Src); 298 g.drawImage(dst, 0, 0, null); 299 } finally { 300 g.dispose(); 301 } 302 } 303 304 return origDst; 305 } 306 307 /** 308 * Transforms the source <CODE>Raster</CODE> and stores the results in 309 * the destination <CODE>Raster</CODE>. This operation performs the 310 * transform band by band. 311 * <p> 312 * If the destination <CODE>Raster</CODE> is null, a new 313 * <CODE>Raster</CODE> is created. 314 * An <CODE>IllegalArgumentException</CODE> may be thrown if the source is 315 * the same as the destination or if the number of bands in 316 * the source is not equal to the number of bands in the 317 * destination. 318 * <p> 319 * The coordinates of the rectangle returned by 320 * <code>getBounds2D(Raster)</code> 321 * are not necessarily the same as the coordinates of the 322 * <code>WritableRaster</code> returned by this method. If the 323 * upper-left corner coordinates of rectangle are negative then 324 * this part of the rectangle is not drawn. If the coordinates 325 * of the rectangle are positive then the filtered image is drawn at 326 * that position in the destination <code>Raster</code>. 327 * <p> 328 * @param src The <CODE>Raster</CODE> to transform. 329 * @param dst The <CODE>Raster</CODE> in which to store the results of the 330 * transformation. 331 * 332 * @return The transformed <CODE>Raster</CODE>. 333 * 334 * @throws ImagingOpException if the raster cannot be transformed 335 * because of a data-processing error that might be 336 * caused by an invalid image format, tile format, or 337 * image-processing operation, or any other unsupported 338 * operation. 339 */ 340 public final WritableRaster filter(Raster src, WritableRaster dst) { 341 if (src == null) { 342 throw new NullPointerException("src image is null"); 343 } 344 if (dst == null) { 345 dst = createCompatibleDestRaster(src); 346 } 347 if (src == dst) { 348 throw new IllegalArgumentException("src image cannot be the "+ 349 "same as the dst image"); 350 } 351 if (src.getNumBands() != dst.getNumBands()) { 352 throw new IllegalArgumentException("Number of src bands ("+ 353 src.getNumBands()+ 354 ") does not match number of "+ 355 " dst bands ("+ 356 dst.getNumBands()+")"); 357 } 358 359 if (ImagingLib.filter(this, src, dst) == null) { 360 throw new ImagingOpException ("Unable to transform src image"); 361 } 362 return dst; 363 } 364 365 /** 366 * Returns the bounding box of the transformed destination. The 367 * rectangle returned is the actual bounding box of the 368 * transformed points. The coordinates of the upper-left corner 369 * of the returned rectangle might not be (0, 0). 370 * 371 * @param src The <CODE>BufferedImage</CODE> to be transformed. 372 * 373 * @return The <CODE>Rectangle2D</CODE> representing the destination's 374 * bounding box. 375 */ 376 public final Rectangle2D getBounds2D (BufferedImage src) { 377 return getBounds2D(src.getRaster()); 378 } 379 380 /** 381 * Returns the bounding box of the transformed destination. The 382 * rectangle returned will be the actual bounding box of the 383 * transformed points. The coordinates of the upper-left corner 384 * of the returned rectangle might not be (0, 0). 385 * 386 * @param src The <CODE>Raster</CODE> to be transformed. 387 * 388 * @return The <CODE>Rectangle2D</CODE> representing the destination's 389 * bounding box. 390 */ 391 public final Rectangle2D getBounds2D (Raster src) { 392 int w = src.getWidth(); 393 int h = src.getHeight(); 394 395 // Get the bounding box of the src and transform the corners 396 float[] pts = {0, 0, w, 0, w, h, 0, h}; 397 xform.transform(pts, 0, pts, 0, 4); 398 399 // Get the min, max of the dst 400 float fmaxX = pts[0]; 401 float fmaxY = pts[1]; 402 float fminX = pts[0]; 403 float fminY = pts[1]; 404 for (int i=2; i < 8; i+=2) { 405 if (pts[i] > fmaxX) { 406 fmaxX = pts[i]; 407 } 408 else if (pts[i] < fminX) { 409 fminX = pts[i]; 410 } 411 if (pts[i+1] > fmaxY) { 412 fmaxY = pts[i+1]; 413 } 414 else if (pts[i+1] < fminY) { 415 fminY = pts[i+1]; 416 } 417 } 418 419 return new Rectangle2D.Float(fminX, fminY, fmaxX-fminX, fmaxY-fminY); 420 } 421 422 /** 423 * Creates a zeroed destination image with the correct size and number of 424 * bands. A <CODE>RasterFormatException</CODE> may be thrown if the 425 * transformed width or height is equal to 0. 426 * <p> 427 * If <CODE>destCM</CODE> is null, 428 * an appropriate <CODE>ColorModel</CODE> is used; this 429 * <CODE>ColorModel</CODE> may have 430 * an alpha channel even if the source <CODE>ColorModel</CODE> is opaque. 431 * 432 * @param src The <CODE>BufferedImage</CODE> to be transformed. 433 * @param destCM <CODE>ColorModel</CODE> of the destination. If null, 434 * an appropriate <CODE>ColorModel</CODE> is used. 435 * 436 * @return The zeroed destination image. 437 */ 438 public BufferedImage createCompatibleDestImage (BufferedImage src, 439 ColorModel destCM) { 440 BufferedImage image; 441 Rectangle r = getBounds2D(src).getBounds(); 442 443 // If r.x (or r.y) is < 0, then we want to only create an image 444 // that is in the positive range. 445 // If r.x (or r.y) is > 0, then we need to create an image that 446 // includes the translation. 447 int w = r.x + r.width; 448 int h = r.y + r.height; 449 if (w <= 0) { 450 throw new RasterFormatException("Transformed width ("+w+ 451 ") is less than or equal to 0."); 452 } 453 if (h <= 0) { 454 throw new RasterFormatException("Transformed height ("+h+ 455 ") is less than or equal to 0."); 456 } 457 458 if (destCM == null) { 459 ColorModel cm = src.getColorModel(); 460 if (interpolationType != TYPE_NEAREST_NEIGHBOR && 461 (cm instanceof IndexColorModel || 462 cm.getTransparency() == Transparency.OPAQUE)) 463 { 464 image = new BufferedImage(w, h, 465 BufferedImage.TYPE_INT_ARGB); 466 } 467 else { 468 image = new BufferedImage(cm, 469 src.getRaster().createCompatibleWritableRaster(w,h), 470 cm.isAlphaPremultiplied(), null); 471 } 472 } 473 else { 474 image = new BufferedImage(destCM, 475 destCM.createCompatibleWritableRaster(w,h), 476 destCM.isAlphaPremultiplied(), null); 477 } 478 479 return image; 480 } 481 482 /** 483 * Creates a zeroed destination <CODE>Raster</CODE> with the correct size 484 * and number of bands. A <CODE>RasterFormatException</CODE> may be thrown 485 * if the transformed width or height is equal to 0. 486 * 487 * @param src The <CODE>Raster</CODE> to be transformed. 488 * 489 * @return The zeroed destination <CODE>Raster</CODE>. 490 */ 491 public WritableRaster createCompatibleDestRaster (Raster src) { 492 Rectangle2D r = getBounds2D(src); 493 494 return src.createCompatibleWritableRaster((int)r.getX(), 495 (int)r.getY(), 496 (int)r.getWidth(), 497 (int)r.getHeight()); 498 } 499 500 /** 501 * Returns the location of the corresponding destination point given a 502 * point in the source. If <CODE>dstPt</CODE> is specified, it 503 * is used to hold the return value. 504 * 505 * @param srcPt The <code>Point2D</code> that represents the source 506 * point. 507 * @param dstPt The <CODE>Point2D</CODE> in which to store the result. 508 * 509 * @return The <CODE>Point2D</CODE> in the destination that corresponds to 510 * the specified point in the source. 511 */ 512 public final Point2D getPoint2D (Point2D srcPt, Point2D dstPt) { 513 return xform.transform (srcPt, dstPt); 514 } 515 516 /** 517 * Returns the affine transform used by this transform operation. 518 * 519 * @return The <CODE>AffineTransform</CODE> associated with this op. 520 */ 521 public final AffineTransform getTransform() { 522 return (AffineTransform) xform.clone(); 523 } 524 525 /** 526 * Returns the rendering hints used by this transform operation. 527 * 528 * @return The <CODE>RenderingHints</CODE> object associated with this op. 529 */ 530 public final RenderingHints getRenderingHints() { 531 if (hints == null) { 532 Object val; 533 switch(interpolationType) { 534 case TYPE_NEAREST_NEIGHBOR: 535 val = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR; 536 break; 537 case TYPE_BILINEAR: 538 val = RenderingHints.VALUE_INTERPOLATION_BILINEAR; 539 break; 540 case TYPE_BICUBIC: 541 val = RenderingHints.VALUE_INTERPOLATION_BICUBIC; 542 break; 543 default: 544 // Should never get here 545 throw new InternalError("Unknown interpolation type "+ 546 interpolationType); 547 548 } 549 hints = new RenderingHints(RenderingHints.KEY_INTERPOLATION, val); 550 } 551 552 return hints; 553 } 554 555 // We need to be able to invert the transform if we want to 556 // transform the image. If the determinant of the matrix is 0, 557 // then we can't invert the transform. 558 void validateTransform(AffineTransform xform) { 559 if (Math.abs(xform.getDeterminant()) <= Double.MIN_VALUE) { 560 throw new ImagingOpException("Unable to invert transform "+xform); 561 } 562 } 563 }