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