1 /*
   2  * Copyright (c) 1997, 2014, 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(RenderingHints.KEY_INTERPOLATION);
 114             if (value == null) {
 115                 value = hints.get(RenderingHints.KEY_RENDERING);
 116                 if (value == RenderingHints.VALUE_RENDER_SPEED) {
 117                     interpolationType = TYPE_NEAREST_NEIGHBOR;
 118                 }
 119                 else if (value == RenderingHints.VALUE_RENDER_QUALITY) {
 120                     interpolationType = TYPE_BILINEAR;
 121                 }
 122             }
 123             else if (value == RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR) {
 124                 interpolationType = TYPE_NEAREST_NEIGHBOR;
 125             }
 126             else if (value == RenderingHints.VALUE_INTERPOLATION_BILINEAR) {
 127                 interpolationType = TYPE_BILINEAR;
 128             }
 129             else if (value == RenderingHints.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                                       (AffineTransform.TYPE_MASK_ROTATION|
 239                                        AffineTransform.TYPE_GENERAL_TRANSFORM))
 240                                      != 0);
 241                 if (! needTrans &&
 242                     type != AffineTransform.TYPE_TRANSLATION &&
 243                     type != AffineTransform.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,&nbsp;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,&nbsp;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 }