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