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