1 /*
   2  * Copyright (c) 1997, 2018, 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.color.ColorSpace;
  29 import java.awt.geom.Rectangle2D;
  30 import java.awt.AlphaComposite;
  31 import java.awt.Graphics2D;
  32 import java.awt.Rectangle;
  33 import java.awt.geom.Point2D;
  34 import java.awt.RenderingHints;
  35 import sun.awt.image.ImagingLib;
  36 
  37 /**
  38  * This class performs a pixel-by-pixel rescaling of the data in the
  39  * source image by multiplying the sample values for each pixel by a scale
  40  * factor and then adding an offset. The scaled sample values are clipped
  41  * to the minimum/maximum representable in the destination image.
  42  * <p>
  43  * The pseudo code for the rescaling operation is as follows:
  44  * <pre>
  45  *for each pixel from Source object {
  46  *    for each band/component of the pixel {
  47  *        dstElement = (srcElement*scaleFactor) + offset
  48  *    }
  49  *}
  50  * </pre>
  51  * <p>
  52  * For Rasters, rescaling operates on bands.  The number of
  53  * sets of scaling constants may be one, in which case the same constants
  54  * are applied to all bands, or it must equal the number of Source
  55  * Raster bands.
  56  * <p>
  57  * For BufferedImages, rescaling operates on color and alpha components.
  58  * The number of sets of scaling constants may be one, in which case the
  59  * same constants are applied to all color (but not alpha) components.
  60  * Otherwise, the  number of sets of scaling constants may
  61  * equal the number of Source color components, in which case no
  62  * rescaling of the alpha component (if present) is performed.
  63  * If neither of these cases apply, the number of sets of scaling constants
  64  * must equal the number of Source color components plus alpha components,
  65  * in which case all color and alpha components are rescaled.
  66  * <p>
  67  * BufferedImage sources with premultiplied alpha data are treated in the same
  68  * manner as non-premultiplied images for purposes of rescaling.  That is,
  69  * the rescaling is done per band on the raw data of the BufferedImage source
  70  * without regard to whether the data is premultiplied.  If a color conversion
  71  * is required to the destination ColorModel, the premultiplied state of
  72  * both source and destination will be taken into account for this step.
  73  * <p>
  74  * Images with an IndexColorModel cannot be rescaled.
  75  * <p>
  76  * If a RenderingHints object is specified in the constructor, the
  77  * color rendering hint and the dithering hint may be used when color
  78  * conversion is required.
  79  * <p>
  80  * Note that in-place operation is allowed (i.e. the source and destination can
  81  * be the same object).
  82  * @see java.awt.RenderingHints#KEY_COLOR_RENDERING
  83  * @see java.awt.RenderingHints#KEY_DITHERING
  84  */
  85 public class RescaleOp implements BufferedImageOp, RasterOp {
  86     float[] scaleFactors;
  87     float[] offsets;
  88     int length = 0;
  89     RenderingHints hints;
  90 
  91     private int srcNbits;
  92     private int dstNbits;
  93 
  94 
  95     /**
  96      * Constructs a new RescaleOp with the desired scale factors
  97      * and offsets.  The length of the scaleFactor and offset arrays
  98      * must meet the restrictions stated in the class comments above.
  99      * The RenderingHints argument may be null.
 100      * @param scaleFactors the specified scale factors
 101      * @param offsets the specified offsets
 102      * @param hints the specified {@code RenderingHints}, or
 103      *        {@code null}
 104      */
 105     public RescaleOp (float[] scaleFactors, float[] offsets,
 106                       RenderingHints hints) {
 107         length = scaleFactors.length;
 108         if (length > offsets.length) length = offsets.length;
 109 
 110         this.scaleFactors = new float[length];
 111         this.offsets      = new float[length];
 112         for (int i=0; i < length; i++) {
 113             this.scaleFactors[i] = scaleFactors[i];
 114             this.offsets[i]      = offsets[i];
 115         }
 116         this.hints = hints;
 117     }
 118 
 119     /**
 120      * Constructs a new RescaleOp with the desired scale factor
 121      * and offset.  The scaleFactor and offset will be applied to
 122      * all bands in a source Raster and to all color (but not alpha)
 123      * components in a BufferedImage.
 124      * The RenderingHints argument may be null.
 125      * @param scaleFactor the specified scale factor
 126      * @param offset the specified offset
 127      * @param hints the specified {@code RenderingHints}, or
 128      *        {@code null}
 129      */
 130     public RescaleOp (float scaleFactor, float offset, RenderingHints hints) {
 131         length = 1;
 132         this.scaleFactors = new float[1];
 133         this.offsets      = new float[1];
 134         this.scaleFactors[0] = scaleFactor;
 135         this.offsets[0]       = offset;
 136         this.hints = hints;
 137     }
 138 
 139     /**
 140      * Returns the scale factors in the given array. The array is also
 141      * returned for convenience.  If scaleFactors is null, a new array
 142      * will be allocated.
 143      * @param scaleFactors the array to contain the scale factors of
 144      *        this {@code RescaleOp}
 145      * @return the scale factors of this {@code RescaleOp}.
 146      */
 147     public final float[] getScaleFactors (float[] scaleFactors) {
 148         if (scaleFactors == null) {
 149             return this.scaleFactors.clone();
 150         }
 151         System.arraycopy (this.scaleFactors, 0, scaleFactors, 0,
 152                           Math.min(this.scaleFactors.length,
 153                                    scaleFactors.length));
 154         return scaleFactors;
 155     }
 156 
 157     /**
 158      * Returns the offsets in the given array. The array is also returned
 159      * for convenience.  If offsets is null, a new array
 160      * will be allocated.
 161      * @param offsets the array to contain the offsets of
 162      *        this {@code RescaleOp}
 163      * @return the offsets of this {@code RescaleOp}.
 164      */
 165     public final float[] getOffsets(float[] offsets) {
 166         if (offsets == null) {
 167             return this.offsets.clone();
 168         }
 169 
 170         System.arraycopy (this.offsets, 0, offsets, 0,
 171                           Math.min(this.offsets.length, offsets.length));
 172         return offsets;
 173     }
 174 
 175     /**
 176      * Returns the number of scaling factors and offsets used in this
 177      * RescaleOp.
 178      * @return the number of scaling factors and offsets of this
 179      *         {@code RescaleOp}.
 180      */
 181     public final int getNumFactors() {
 182         return length;
 183     }
 184 
 185 
 186     /**
 187      * Creates a ByteLookupTable to implement the rescale.
 188      * The table may have either a SHORT or BYTE input.
 189      * @param nElems    Number of elements the table is to have.
 190      *                  This will generally be 256 for byte and
 191      *                  65536 for short.
 192      */
 193     private ByteLookupTable createByteLut(float[] scale,
 194                                           float[] off,
 195                                           int   nBands,
 196                                           int   nElems) {
 197 
 198         byte[][]        lutData = new byte[nBands][nElems];
 199         int band;
 200 
 201         for (band=0; band<scale.length; band++) {
 202             float  bandScale   = scale[band];
 203             float  bandOff     = off[band];
 204             byte[] bandLutData = lutData[band];
 205             for (int i=0; i<nElems; i++) {
 206                 int val = (int)(i*bandScale + bandOff);
 207                 if ((val & 0xffffff00) != 0) {
 208                     if (val < 0) {
 209                         val = 0;
 210                     } else {
 211                         val = 255;
 212                     }
 213                 }
 214                 bandLutData[i] = (byte)val;
 215             }
 216 
 217         }
 218         int maxToCopy = (nBands == 4 && scale.length == 4) ? 4 : 3;
 219         while (band < lutData.length && band < maxToCopy) {
 220            System.arraycopy(lutData[band-1], 0, lutData[band], 0, nElems);
 221            band++;
 222         }
 223         if (nBands == 4 && band < nBands) {
 224            byte[] bandLutData = lutData[band];
 225            for (int i=0; i<nElems; i++) {
 226               bandLutData[i] = (byte)i;
 227            }
 228         }
 229 
 230         return new ByteLookupTable(0, lutData);
 231     }
 232 
 233     /**
 234      * Creates a ShortLookupTable to implement the rescale.
 235      * The table may have either a SHORT or BYTE input.
 236      * @param nElems    Number of elements the table is to have.
 237      *                  This will generally be 256 for byte and
 238      *                  65536 for short.
 239      */
 240     private ShortLookupTable createShortLut(float[] scale,
 241                                             float[] off,
 242                                             int   nBands,
 243                                             int   nElems) {
 244 
 245         short[][]        lutData = new short[nBands][nElems];
 246         int band = 0;
 247 
 248         for (band=0; band<scale.length; band++) {
 249             float   bandScale   = scale[band];
 250             float   bandOff     = off[band];
 251             short[] bandLutData = lutData[band];
 252             for (int i=0; i<nElems; i++) {
 253                 int val = (int)(i*bandScale + bandOff);
 254                 if ((val & 0xffff0000) != 0) {
 255                     if (val < 0) {
 256                         val = 0;
 257                     } else {
 258                         val = 65535;
 259                     }
 260                 }
 261                 bandLutData[i] = (short)val;
 262             }
 263         }
 264         int maxToCopy = (nBands == 4 && scale.length == 4) ? 4 : 3;
 265         while (band < lutData.length && band < maxToCopy) {
 266            System.arraycopy(lutData[band-1], 0, lutData[band], 0, nElems);
 267            band++;
 268         }
 269         if (nBands == 4 && band < nBands) {
 270            short[] bandLutData = lutData[band];
 271            for (int i=0; i<nElems; i++) {
 272               bandLutData[i] = (short)i;
 273            }
 274         }
 275 
 276         return new ShortLookupTable(0, lutData);
 277     }
 278 
 279 
 280     /**
 281      * Determines if the rescale can be performed as a lookup.
 282      * The dst must be a byte or short type.
 283      * The src must be less than 16 bits.
 284      * All source band sizes must be the same and all dst band sizes
 285      * must be the same.
 286      */
 287     private boolean canUseLookup(Raster src, Raster dst) {
 288 
 289         //
 290         // Check that the src datatype is either a BYTE or SHORT
 291         //
 292         int datatype = src.getDataBuffer().getDataType();
 293         if(datatype != DataBuffer.TYPE_BYTE &&
 294            datatype != DataBuffer.TYPE_USHORT) {
 295             return false;
 296         }
 297 
 298         //
 299         // Check dst sample sizes. All must be 8 or 16 bits.
 300         //
 301         SampleModel dstSM = dst.getSampleModel();
 302         dstNbits = dstSM.getSampleSize(0);
 303 
 304         if (!(dstNbits == 8 || dstNbits == 16)) {
 305             return false;
 306         }
 307         for (int i=1; i<src.getNumBands(); i++) {
 308             int bandSize = dstSM.getSampleSize(i);
 309             if (bandSize != dstNbits) {
 310                 return false;
 311             }
 312         }
 313 
 314         //
 315         // Check src sample sizes. All must be the same size
 316         //
 317         SampleModel srcSM = src.getSampleModel();
 318         srcNbits = srcSM.getSampleSize(0);
 319         if (srcNbits > 16) {
 320             return false;
 321         }
 322         for (int i=1; i<src.getNumBands(); i++) {
 323             int bandSize = srcSM.getSampleSize(i);
 324             if (bandSize != srcNbits) {
 325                 return false;
 326             }
 327         }
 328 
 329       if (dstSM instanceof ComponentSampleModel) {
 330            ComponentSampleModel dsm = (ComponentSampleModel)dstSM;
 331            if (dsm.getPixelStride() != dst.getNumBands()) {
 332                return false;
 333            }
 334         }
 335         if (srcSM instanceof ComponentSampleModel) {
 336            ComponentSampleModel csm = (ComponentSampleModel)srcSM;
 337            if (csm.getPixelStride() != src.getNumBands()) {
 338                return false;
 339            }
 340         }
 341 
 342         return true;
 343     }
 344 
 345     /**
 346      * Rescales the source BufferedImage.
 347      * If the color model in the source image is not the same as that
 348      * in the destination image, the pixels will be converted
 349      * in the destination.  If the destination image is null,
 350      * a BufferedImage will be created with the source ColorModel.
 351      * An IllegalArgumentException may be thrown if the number of
 352      * scaling factors/offsets in this object does not meet the
 353      * restrictions stated in the class comments above, or if the
 354      * source image has an IndexColorModel.
 355      * @param src the {@code BufferedImage} to be filtered
 356      * @param dst the destination for the filtering operation
 357      *            or {@code null}
 358      * @return the filtered {@code BufferedImage}.
 359      * @throws IllegalArgumentException if the {@code ColorModel}
 360      *         of {@code src} is an {@code IndexColorModel},
 361      *         or if the number of scaling factors and offsets in this
 362      *         {@code RescaleOp} do not meet the requirements
 363      *         stated in the class comments, or if the source and
 364      *         destination images differ in size.
 365      */
 366     public final BufferedImage filter (BufferedImage src, BufferedImage dst) {
 367         ColorModel srcCM = src.getColorModel();
 368         ColorModel dstCM;
 369         int numSrcColorComp = srcCM.getNumColorComponents();
 370         int scaleConst = length;
 371 
 372         if (srcCM instanceof IndexColorModel) {
 373             throw new
 374                 IllegalArgumentException("Rescaling cannot be "+
 375                                          "performed on an indexed image");
 376         }
 377         if (scaleConst != 1 && scaleConst != numSrcColorComp &&
 378             scaleConst != srcCM.getNumComponents())
 379         {
 380             throw new IllegalArgumentException("Number of scaling constants "+
 381                                                "does not equal the number of"+
 382                                                " of color or color/alpha "+
 383                                                " components");
 384         }
 385 
 386         boolean needToConvert = false;
 387         boolean needToDraw = false;
 388 
 389         // Include alpha
 390         if (scaleConst > numSrcColorComp && srcCM.hasAlpha()) {
 391             scaleConst = numSrcColorComp+1;
 392         }
 393 
 394         int width = src.getWidth();
 395         int height = src.getHeight();
 396 
 397         BufferedImage origDst = dst;
 398         if (dst == null) {
 399             dst = createCompatibleDestImage(src, null);
 400             dstCM = srcCM;
 401         }
 402         else {
 403             if (width != dst.getWidth()) {
 404                 throw new
 405                     IllegalArgumentException("Src width ("+width+
 406                                              ") not equal to dst width ("+
 407                                              dst.getWidth()+")");
 408             }
 409             if (height != dst.getHeight()) {
 410                 throw new
 411                     IllegalArgumentException("Src height ("+height+
 412                                              ") not equal to dst height ("+
 413                                              dst.getHeight()+")");
 414             }
 415 
 416             dstCM = dst.getColorModel();
 417             if(srcCM.getColorSpace().getType() !=
 418                  dstCM.getColorSpace().getType()) {
 419                 needToConvert = true;
 420                 dst = createCompatibleDestImage(src, null);
 421             }
 422 
 423         }
 424 
 425         //
 426         // Try to use a native BI rescale operation first
 427         //
 428         if (ImagingLib.filter(this, src, dst) == null) {
 429             if (src.getRaster().getNumBands() !=
 430                 dst.getRaster().getNumBands()) {
 431                 needToDraw = true;
 432                 dst = createCompatibleDestImage(src, null);
 433             }
 434 
 435             //
 436             // Native BI rescale failed - convert to rasters
 437             //
 438             WritableRaster srcRaster = src.getRaster();
 439             WritableRaster dstRaster = dst.getRaster();
 440 
 441             //
 442             // Call the raster filter method
 443             //
 444             filterRasterImpl(srcRaster, dstRaster, scaleConst, false);
 445         }
 446 
 447         if (needToDraw) {
 448              Graphics2D g = origDst.createGraphics();
 449              g.setComposite(AlphaComposite.Src);
 450              g.drawImage(dst, 0, 0, width, height, null);
 451              g.dispose();
 452         }
 453         if (needToConvert) {
 454             // ColorModels are not the same
 455             ColorConvertOp ccop = new ColorConvertOp(hints);
 456             dst = ccop.filter(dst, origDst);
 457         }
 458         return dst;
 459     }
 460 
 461     /**
 462      * Rescales the pixel data in the source Raster.
 463      * If the destination Raster is null, a new Raster will be created.
 464      * The source and destination must have the same number of bands.
 465      * Otherwise, an IllegalArgumentException is thrown.
 466      * Note that the number of scaling factors/offsets in this object must
 467      * meet the restrictions stated in the class comments above.
 468      * Otherwise, an IllegalArgumentException is thrown.
 469      * @param src the {@code Raster} to be filtered
 470      * @param dst the destination for the filtering operation
 471      *            or {@code null}
 472      * @return the filtered {@code WritableRaster}.
 473      * @throws IllegalArgumentException if {@code src} and
 474      *         {@code dst} do not have the same number of bands,
 475      *         or if the number of scaling factors and offsets in this
 476      *         {@code RescaleOp} do not meet the requirements
 477      *         stated in the class comments, or if the source and
 478      *         destination rasters differ in size.
 479      */
 480     public final WritableRaster filter (Raster src, WritableRaster dst)  {
 481         return filterRasterImpl(src, dst, length, true);
 482     }
 483 
 484     private WritableRaster filterRasterImpl(Raster src, WritableRaster dst,
 485                                             int scaleConst, boolean sCheck) {
 486         int numBands = src.getNumBands();
 487         int width  = src.getWidth();
 488         int height = src.getHeight();
 489         int[] srcPix = null;
 490         int step = 0;
 491         int tidx = 0;
 492 
 493         // Create a new destination Raster, if needed
 494         if (dst == null) {
 495             dst = createCompatibleDestRaster(src);
 496         }
 497         else if (height != dst.getHeight() || width != dst.getWidth()) {
 498             throw new
 499                IllegalArgumentException("Width or height of Rasters do not "+
 500                                         "match");
 501         }
 502         else if (numBands != dst.getNumBands()) {
 503             // Make sure that the number of bands are equal
 504             throw new IllegalArgumentException("Number of bands in src "
 505                             + numBands
 506                             + " does not equal number of bands in dest "
 507                             + dst.getNumBands());
 508         }
 509 
 510         // Make sure that the arrays match
 511         // Make sure that the low/high/constant arrays match
 512         if (sCheck && scaleConst != 1 && scaleConst != src.getNumBands()) {
 513             throw new IllegalArgumentException("Number of scaling constants "+
 514                                                "does not equal the number of"+
 515                                                " of bands in the src raster");
 516         }
 517 
 518         //
 519         // Try for a native raster rescale first
 520         //
 521         if (ImagingLib.filter(this, src, dst) != null) {
 522             return dst;
 523         }
 524 
 525         //
 526         // Native raster rescale failed.
 527         // Try to see if a lookup operation can be used
 528         //
 529         if (canUseLookup(src, dst)) {
 530             int srcNgray = (1 << srcNbits);
 531             int dstNgray = (1 << dstNbits);
 532 
 533             if (dstNgray == 256) {
 534                 ByteLookupTable lut = createByteLut(scaleFactors, offsets,
 535                                                     numBands, srcNgray);
 536                 LookupOp op = new LookupOp(lut, hints);
 537                 op.filter(src, dst);
 538             } else {
 539                 ShortLookupTable lut = createShortLut(scaleFactors, offsets,
 540                                                       numBands, srcNgray);
 541                 LookupOp op = new LookupOp(lut, hints);
 542                 op.filter(src, dst);
 543             }
 544         } else {
 545             //
 546             // Fall back to the slow code
 547             //
 548             if (scaleConst > 1) {
 549                 step = 1;
 550             }
 551 
 552             int sminX = src.getMinX();
 553             int sY = src.getMinY();
 554             int dminX = dst.getMinX();
 555             int dY = dst.getMinY();
 556             int sX;
 557             int dX;
 558 
 559             //
 560             //  Determine bits per band to determine maxval for clamps.
 561             //  The min is assumed to be zero.
 562             //  REMIND: This must change if we ever support signed data types.
 563             //
 564             int nbits;
 565             int[] dstMax = new int[numBands];
 566             int[] dstMask = new int[numBands];
 567             SampleModel dstSM = dst.getSampleModel();
 568             for (int z=0; z<numBands; z++) {
 569                 nbits = dstSM.getSampleSize(z);
 570                 dstMax[z] = (1 << nbits) - 1;
 571                 dstMask[z] = ~(dstMax[z]);
 572             }
 573 
 574             int val;
 575             for (int y=0; y < height; y++, sY++, dY++) {
 576                 dX = dminX;
 577                 sX = sminX;
 578                 for (int x = 0; x < width; x++, sX++, dX++) {
 579                     // Get data for all bands at this x,y position
 580                     srcPix = src.getPixel(sX, sY, srcPix);
 581                     tidx = 0;
 582                     for (int z=0; z<numBands; z++, tidx += step) {
 583                         if ((scaleConst == 1 || scaleConst == 3) &&
 584                             (z == 3) && (numBands == 4)) {
 585                            val = srcPix[z];
 586                         } else {
 587                             val = (int)(srcPix[z]*scaleFactors[tidx]
 588                                               + offsets[tidx]);
 589 
 590                         }
 591                         // Clamp
 592                         if ((val & dstMask[z]) != 0) {
 593                             if (val < 0) {
 594                                 val = 0;
 595                             } else {
 596                                 val = dstMax[z];
 597                             }
 598                         }
 599                         srcPix[z] = val;
 600 
 601                     }
 602 
 603                     // Put it back for all bands
 604                     dst.setPixel(dX, dY, srcPix);
 605                 }
 606             }
 607         }
 608         return dst;
 609     }
 610 
 611     /**
 612      * Returns the bounding box of the rescaled destination image.  Since
 613      * this is not a geometric operation, the bounding box does not
 614      * change.
 615      */
 616     public final Rectangle2D getBounds2D (BufferedImage src) {
 617          return getBounds2D(src.getRaster());
 618     }
 619 
 620     /**
 621      * Returns the bounding box of the rescaled destination Raster.  Since
 622      * this is not a geometric operation, the bounding box does not
 623      * change.
 624      * @param src the rescaled destination {@code Raster}
 625      * @return the bounds of the specified {@code Raster}.
 626      */
 627     public final Rectangle2D getBounds2D (Raster src) {
 628         return src.getBounds();
 629     }
 630 
 631     /**
 632      * Creates a zeroed destination image with the correct size and number of
 633      * bands.
 634      * @param src       Source image for the filter operation.
 635      * @param destCM    ColorModel of the destination.  If null, the
 636      *                  ColorModel of the source will be used.
 637      * @return the zeroed-destination image.
 638      */
 639     public BufferedImage createCompatibleDestImage (BufferedImage src,
 640                                                     ColorModel destCM) {
 641         BufferedImage image;
 642         if (destCM == null) {
 643             ColorModel cm = src.getColorModel();
 644             image = new BufferedImage(cm,
 645                                       src.getRaster().createCompatibleWritableRaster(),
 646                                       cm.isAlphaPremultiplied(),
 647                                       null);
 648         }
 649         else {
 650             int w = src.getWidth();
 651             int h = src.getHeight();
 652             image = new BufferedImage (destCM,
 653                                    destCM.createCompatibleWritableRaster(w, h),
 654                                    destCM.isAlphaPremultiplied(), null);
 655         }
 656 
 657         return image;
 658     }
 659 
 660     /**
 661      * Creates a zeroed-destination {@code Raster} with the correct
 662      * size and number of bands, given this source.
 663      * @param src       the source {@code Raster}
 664      * @return the zeroed-destination {@code Raster}.
 665      */
 666     public WritableRaster createCompatibleDestRaster (Raster src) {
 667         return src.createCompatibleWritableRaster(src.getWidth(), src.getHeight());
 668     }
 669 
 670     /**
 671      * Returns the location of the destination point given a
 672      * point in the source.  If dstPt is non-null, it will
 673      * be used to hold the return value.  Since this is not a geometric
 674      * operation, the srcPt will equal the dstPt.
 675      * @param srcPt a point in the source image
 676      * @param dstPt the destination point or {@code null}
 677      * @return the location of the destination point.
 678      */
 679     public final Point2D getPoint2D (Point2D srcPt, Point2D dstPt) {
 680         if (dstPt == null) {
 681             dstPt = new Point2D.Float();
 682         }
 683         dstPt.setLocation(srcPt.getX(), srcPt.getY());
 684         return dstPt;
 685     }
 686 
 687     /**
 688      * Returns the rendering hints for this op.
 689      * @return the rendering hints of this {@code RescaleOp}.
 690      */
 691     public final RenderingHints getRenderingHints() {
 692         return hints;
 693     }
 694 }