RescaleOp.java revision 12839:f618cefbe1e3
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
26package java.awt.image;
27
28import java.awt.color.ColorSpace;
29import java.awt.geom.Rectangle2D;
30import java.awt.Rectangle;
31import java.awt.geom.Point2D;
32import java.awt.RenderingHints;
33import 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 */
83public 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    public final float[] getScaleFactors (float scaleFactors[]) {
146        if (scaleFactors == null) {
147            return 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    public final float[] getOffsets(float offsets[]) {
164        if (offsets == null) {
165            return 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    public final 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 numSrcColorComp = srcCM.getNumColorComponents();
330        int scaleConst = length;
331
332        if (srcCM instanceof IndexColorModel) {
333            throw new
334                IllegalArgumentException("Rescaling cannot be "+
335                                         "performed on an indexed image");
336        }
337        if (scaleConst != 1 && scaleConst != numSrcColorComp &&
338            scaleConst != 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 (scaleConst > numSrcColorComp && srcCM.hasAlpha()) {
350            scaleConst = numSrcColorComp+1;
351        }
352
353        int width = src.getWidth();
354        int height = src.getHeight();
355
356        BufferedImage origDst = dst;
357        if (dst == null) {
358            dst = createCompatibleDestImage(src, null);
359            dstCM = srcCM;
360        }
361        else {
362            if (width != dst.getWidth()) {
363                throw new
364                    IllegalArgumentException("Src width ("+width+
365                                             ") not equal to dst width ("+
366                                             dst.getWidth()+")");
367            }
368            if (height != dst.getHeight()) {
369                throw new
370                    IllegalArgumentException("Src height ("+height+
371                                             ") not equal to dst height ("+
372                                             dst.getHeight()+")");
373            }
374
375            dstCM = dst.getColorModel();
376            if(srcCM.getColorSpace().getType() !=
377               dstCM.getColorSpace().getType()) {
378                needToConvert = true;
379                dst = createCompatibleDestImage(src, null);
380            }
381
382        }
383
384        boolean scaleAlpha = true;
385
386        //
387        // The number of sets of scaling constants may be one,
388        // in which case the same constants are applied to all color
389        // (but NOT alpha) components. Otherwise, the number of sets
390        // of scaling constants may equal the number of Source color
391        // components, in which case NO rescaling of the alpha component
392        // (if present) is performed.
393        //
394        if (numSrcColorComp == scaleConst || scaleConst == 1) {
395            scaleAlpha = false;
396        }
397
398        //
399        // Try to use a native BI rescale operation first
400        //
401        if (ImagingLib.filter(this, src, dst) == null) {
402            //
403            // Native BI rescale failed - convert to rasters
404            //
405            WritableRaster srcRaster = src.getRaster();
406            WritableRaster dstRaster = dst.getRaster();
407
408            if (!scaleAlpha) {
409                if (srcCM.hasAlpha()) {
410                    // Do not rescale Alpha component
411                    int minx = srcRaster.getMinX();
412                    int miny = srcRaster.getMinY();
413                    int[] bands = new int[numSrcColorComp];
414                    for (int i=0; i < numSrcColorComp; i++) {
415                        bands[i] = i;
416                    }
417                    srcRaster =
418                        srcRaster.createWritableChild(minx, miny,
419                                                      srcRaster.getWidth(),
420                                                      srcRaster.getHeight(),
421                                                      minx, miny,
422                                                      bands);
423                }
424                if (dstCM.hasAlpha()) {
425                    int minx = dstRaster.getMinX();
426                    int miny = dstRaster.getMinY();
427                    int[] bands = new int[numSrcColorComp];
428                    for (int i=0; i < numSrcColorComp; i++) {
429                        bands[i] = i;
430                    }
431                    dstRaster =
432                        dstRaster.createWritableChild(minx, miny,
433                                                      dstRaster.getWidth(),
434                                                      dstRaster.getHeight(),
435                                                      minx, miny,
436                                                      bands);
437                }
438            }
439
440            //
441            // Call the raster filter method
442            //
443            filterRasterImpl(srcRaster, dstRaster, scaleConst);
444
445            //
446            // here copy the unscaled src alpha to destination alpha channel
447            //
448            if (!scaleAlpha) {
449                Raster srcAlphaRaster = null;
450                WritableRaster dstAlphaRaster = null;
451
452                if (srcCM.hasAlpha()) {
453                    srcAlphaRaster = src.getAlphaRaster();
454                }
455                if (dstCM.hasAlpha()) {
456                    dstAlphaRaster = dst.getAlphaRaster();
457                    if (srcAlphaRaster != null) {
458                        dstAlphaRaster.setRect(srcAlphaRaster);
459                    } else {
460                        int alpha = 0xff << 24;
461                        for (int cy=0; cy < dst.getHeight(); cy++) {
462                            for (int cx=0; cx < dst.getWidth(); cx++) {
463                                int color = dst.getRGB(cx, cy);
464
465                                dst.setRGB(cx, cy, color | alpha);
466                            }
467                        }
468                    }
469                }
470            }
471        }
472
473        if (needToConvert) {
474            // ColorModels are not the same
475            ColorConvertOp ccop = new ColorConvertOp(hints);
476            dst = ccop.filter(dst, origDst);
477        }
478        return dst;
479    }
480
481    /**
482     * Rescales the pixel data in the source Raster.
483     * If the destination Raster is null, a new Raster will be created.
484     * The source and destination must have the same number of bands.
485     * Otherwise, an IllegalArgumentException is thrown.
486     * Note that the number of scaling factors/offsets in this object must
487     * meet the restrictions stated in the class comments above.
488     * Otherwise, an IllegalArgumentException is thrown.
489     * @param src the <code>Raster</code> to be filtered
490     * @param dst the destination for the filtering operation
491     *            or <code>null</code>
492     * @return the filtered <code>WritableRaster</code>.
493     * @throws IllegalArgumentException if <code>src</code> and
494     *         <code>dst</code> do not have the same number of bands,
495     *         or if the number of scaling factors and offsets in this
496     *         <code>RescaleOp</code> do not meet the requirements
497     *         stated in the class comments.
498     */
499    public final WritableRaster filter (Raster src, WritableRaster dst)  {
500        return filterRasterImpl(src, dst, length);
501    }
502
503    private WritableRaster filterRasterImpl(Raster src, WritableRaster dst, int scaleConst) {
504        int numBands = src.getNumBands();
505        int width  = src.getWidth();
506        int height = src.getHeight();
507        int[] srcPix = null;
508        int step = 0;
509        int tidx = 0;
510
511        // Create a new destination Raster, if needed
512        if (dst == null) {
513            dst = createCompatibleDestRaster(src);
514        }
515        else if (height != dst.getHeight() || width != dst.getWidth()) {
516            throw new
517               IllegalArgumentException("Width or height of Rasters do not "+
518                                        "match");
519        }
520        else if (numBands != dst.getNumBands()) {
521            // Make sure that the number of bands are equal
522            throw new IllegalArgumentException("Number of bands in src "
523                            + numBands
524                            + " does not equal number of bands in dest "
525                            + dst.getNumBands());
526        }
527
528        // Make sure that the arrays match
529        // Make sure that the low/high/constant arrays match
530        if (scaleConst != 1 && scaleConst != src.getNumBands()) {
531            throw new IllegalArgumentException("Number of scaling constants "+
532                                               "does not equal the number of"+
533                                               " of bands in the src raster");
534        }
535
536        //
537        // Try for a native raster rescale first
538        //
539        if (ImagingLib.filter(this, src, dst) != null) {
540            return dst;
541        }
542
543        //
544        // Native raster rescale failed.
545        // Try to see if a lookup operation can be used
546        //
547        if (canUseLookup(src, dst)) {
548            int srcNgray = (1 << srcNbits);
549            int dstNgray = (1 << dstNbits);
550
551            if (dstNgray == 256) {
552                ByteLookupTable lut = createByteLut(scaleFactors, offsets,
553                                                    numBands, srcNgray);
554                LookupOp op = new LookupOp(lut, hints);
555                op.filter(src, dst);
556            } else {
557                ShortLookupTable lut = createShortLut(scaleFactors, offsets,
558                                                      numBands, srcNgray);
559                LookupOp op = new LookupOp(lut, hints);
560                op.filter(src, dst);
561            }
562        } else {
563            //
564            // Fall back to the slow code
565            //
566            if (scaleConst > 1) {
567                step = 1;
568            }
569
570            int sminX = src.getMinX();
571            int sY = src.getMinY();
572            int dminX = dst.getMinX();
573            int dY = dst.getMinY();
574            int sX;
575            int dX;
576
577            //
578            //  Determine bits per band to determine maxval for clamps.
579            //  The min is assumed to be zero.
580            //  REMIND: This must change if we ever support signed data types.
581            //
582            int nbits;
583            int dstMax[] = new int[numBands];
584            int dstMask[] = new int[numBands];
585            SampleModel dstSM = dst.getSampleModel();
586            for (int z=0; z<numBands; z++) {
587                nbits = dstSM.getSampleSize(z);
588                dstMax[z] = (1 << nbits) - 1;
589                dstMask[z] = ~(dstMax[z]);
590            }
591
592            int val;
593            for (int y=0; y < height; y++, sY++, dY++) {
594                dX = dminX;
595                sX = sminX;
596                for (int x = 0; x < width; x++, sX++, dX++) {
597                    // Get data for all bands at this x,y position
598                    srcPix = src.getPixel(sX, sY, srcPix);
599                    tidx = 0;
600                    for (int z=0; z<numBands; z++, tidx += step) {
601                        val = (int)(srcPix[z]*scaleFactors[tidx]
602                                          + offsets[tidx]);
603                        // Clamp
604                        if ((val & dstMask[z]) != 0) {
605                            if (val < 0) {
606                                val = 0;
607                            } else {
608                                val = dstMax[z];
609                            }
610                        }
611                        srcPix[z] = val;
612
613                    }
614
615                    // Put it back for all bands
616                    dst.setPixel(dX, dY, srcPix);
617                }
618            }
619        }
620        return dst;
621    }
622
623    /**
624     * Returns the bounding box of the rescaled destination image.  Since
625     * this is not a geometric operation, the bounding box does not
626     * change.
627     */
628    public final Rectangle2D getBounds2D (BufferedImage src) {
629         return getBounds2D(src.getRaster());
630    }
631
632    /**
633     * Returns the bounding box of the rescaled destination Raster.  Since
634     * this is not a geometric operation, the bounding box does not
635     * change.
636     * @param src the rescaled destination <code>Raster</code>
637     * @return the bounds of the specified <code>Raster</code>.
638     */
639    public final Rectangle2D getBounds2D (Raster src) {
640        return src.getBounds();
641    }
642
643    /**
644     * Creates a zeroed destination image with the correct size and number of
645     * bands.
646     * @param src       Source image for the filter operation.
647     * @param destCM    ColorModel of the destination.  If null, the
648     *                  ColorModel of the source will be used.
649     * @return the zeroed-destination image.
650     */
651    public BufferedImage createCompatibleDestImage (BufferedImage src,
652                                                    ColorModel destCM) {
653        BufferedImage image;
654        if (destCM == null) {
655            ColorModel cm = src.getColorModel();
656            image = new BufferedImage(cm,
657                                      src.getRaster().createCompatibleWritableRaster(),
658                                      cm.isAlphaPremultiplied(),
659                                      null);
660        }
661        else {
662            int w = src.getWidth();
663            int h = src.getHeight();
664            image = new BufferedImage (destCM,
665                                   destCM.createCompatibleWritableRaster(w, h),
666                                   destCM.isAlphaPremultiplied(), null);
667        }
668
669        return image;
670    }
671
672    /**
673     * Creates a zeroed-destination <code>Raster</code> with the correct
674     * size and number of bands, given this source.
675     * @param src       the source <code>Raster</code>
676     * @return the zeroed-destination <code>Raster</code>.
677     */
678    public WritableRaster createCompatibleDestRaster (Raster src) {
679        return src.createCompatibleWritableRaster(src.getWidth(), src.getHeight());
680    }
681
682    /**
683     * Returns the location of the destination point given a
684     * point in the source.  If dstPt is non-null, it will
685     * be used to hold the return value.  Since this is not a geometric
686     * operation, the srcPt will equal the dstPt.
687     * @param srcPt a point in the source image
688     * @param dstPt the destination point or <code>null</code>
689     * @return the location of the destination point.
690     */
691    public final Point2D getPoint2D (Point2D srcPt, Point2D dstPt) {
692        if (dstPt == null) {
693            dstPt = new Point2D.Float();
694        }
695        dstPt.setLocation(srcPt.getX(), srcPt.getY());
696        return dstPt;
697    }
698
699    /**
700     * Returns the rendering hints for this op.
701     * @return the rendering hints of this <code>RescaleOp</code>.
702     */
703    public final RenderingHints getRenderingHints() {
704        return hints;
705    }
706}
707