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