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,&nbsp;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,&nbsp;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 }