001    /* Copyright (C) 2004, 2006  Free Software Foundation
002    
003    This file is part of GNU Classpath.
004    
005    GNU Classpath is free software; you can redistribute it and/or modify
006    it under the terms of the GNU General Public License as published by
007    the Free Software Foundation; either version 2, or (at your option)
008    any later version.
009    
010    GNU Classpath is distributed in the hope that it will be useful, but
011    WITHOUT ANY WARRANTY; without even the implied warranty of
012    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013    General Public License for more details.
014    
015    You should have received a copy of the GNU General Public License
016    along with GNU Classpath; see the file COPYING.  If not, write to the
017    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
018    02110-1301 USA.
019    
020    Linking this library statically or dynamically with other modules is
021    making a combined work based on this library.  Thus, the terms and
022    conditions of the GNU General Public License cover the whole
023    combination.
024    
025    As a special exception, the copyright holders of this library give you
026    permission to link this library with independent modules to produce an
027    executable, regardless of the license terms of these independent
028    modules, and to copy and distribute the resulting executable under
029    terms of your choice, provided that you also meet, for each linked
030    independent module, the terms and conditions of the license of that
031    module.  An independent module is a module which is not derived from
032    or based on this library.  If you modify this library, you may extend
033    this exception to your version of the library, but you are not
034    obligated to do so.  If you do not wish to do so, delete this
035    exception statement from your version. */
036    
037    
038    package java.awt.image;
039    
040    import java.awt.RenderingHints;
041    import java.awt.geom.Point2D;
042    import java.awt.geom.Rectangle2D;
043    import java.util.Arrays;
044    
045    /**
046     * RescaleOp is a filter that changes each pixel by a scaling factor and offset.
047     *
048     * For filtering Rasters, either one scaling factor and offset can be specified,
049     * which will be applied to all bands; or a scaling factor and offset can be
050     * specified for each band.
051     *
052     * For BufferedImages, the scaling may apply to both color and alpha components.
053     * If only one scaling factor is provided, or if the number of factors provided
054     * equals the number of color components, the scaling is performed on all color
055     * components.  Otherwise, the scaling is performed on all components including
056     * alpha.  Alpha premultiplication is ignored.
057     *
058     * After filtering, if color conversion is necessary, the conversion happens,
059     * taking alpha premultiplication into account.
060     *
061     * @author Jerry Quinn (jlquinn@optonline.net)
062     * @author Francis Kung (fkung@redhat.com)
063     */
064    public class RescaleOp implements BufferedImageOp, RasterOp
065    {
066      private float[] scale;
067      private float[] offsets;
068      private RenderingHints hints = null;
069    
070      /**
071       * Create a new RescaleOp object using the given scale factors and offsets.
072       *
073       * The length of the arrays must be equal to the number of bands (or number of
074       * data or color components) of the raster/image that this Op will be used on,
075       * otherwise an IllegalArgumentException will be thrown when calling the
076       * filter method.
077       *
078       * @param scaleFactors an array of scale factors.
079       * @param offsets an array of offsets.
080       * @param hints any rendering hints to use (can be null).
081       * @throws NullPointerException if the scaleFactors or offsets array is null.
082       */
083      public RescaleOp(float[] scaleFactors,
084                       float[] offsets,
085                       RenderingHints hints)
086      {
087        int length = Math.min(scaleFactors.length, offsets.length);
088    
089        scale = new float[length];
090        System.arraycopy(scaleFactors, 0, this.scale, 0, length);
091    
092        this.offsets = new float[length];
093        System.arraycopy(offsets, 0, this.offsets, 0, length);
094    
095        this.hints = hints;
096      }
097    
098      /**
099       * Create a new RescaleOp object using the given scale factor and offset.
100       *
101       * The same scale factor and offset will be used on all bands/components.
102       *
103       * @param scaleFactor the scale factor to use.
104       * @param offset the offset to use.
105       * @param hints any rendering hints to use (can be null).
106       */
107      public RescaleOp(float scaleFactor,
108                       float offset,
109                       RenderingHints hints)
110      {
111        scale = new float[]{ scaleFactor };
112        offsets = new float[]{offset};
113        this.hints = hints;
114      }
115    
116      /**
117       * Returns the scaling factors.  This method accepts an optional array, which
118       * will be used to store the factors if not null (this avoids allocating a
119       * new array).  If this array is too small to hold all the scaling factors,
120       * the array will be filled and the remaining factors discarded.
121       *
122       * @param scaleFactors array to store the scaling factors in (can be null).
123       * @return an array of scaling factors.
124       */
125      public final float[] getScaleFactors(float[] scaleFactors)
126      {
127        if (scaleFactors == null)
128          scaleFactors = new float[scale.length];
129        System.arraycopy(scale, 0, scaleFactors, 0, Math.min(scale.length,
130                                                             scaleFactors.length));
131        return scaleFactors;
132      }
133    
134      /**
135       * Returns the offsets.  This method accepts an optional array, which
136       * will be used to store the offsets if not null (this avoids allocating a
137       * new array).  If this array is too small to hold all the offsets, the array
138       * will be filled and the remaining factors discarded.
139       *
140       * @param offsets array to store the offsets in (can be null).
141       * @return an array of offsets.
142       */
143      public final float[] getOffsets(float[] offsets)
144      {
145        if (offsets == null)
146          offsets = new float[this.offsets.length];
147        System.arraycopy(this.offsets, 0, offsets, 0, Math.min(this.offsets.length,
148                                                               offsets.length));
149        return offsets;
150      }
151    
152      /**
153       * Returns the number of scaling factors / offsets.
154       *
155       * @return the number of scaling factors / offsets.
156       */
157      public final int getNumFactors()
158      {
159        return scale.length;
160      }
161    
162      /* (non-Javadoc)
163       * @see java.awt.image.BufferedImageOp#getRenderingHints()
164       */
165      public final RenderingHints getRenderingHints()
166      {
167        return hints;
168      }
169    
170      /**
171       * Converts the source image using the scale factors and offsets specified in
172       * the constructor.  The resulting image is stored in the destination image if
173       * one is provided; otherwise a new BufferedImage is created and returned.
174       *
175       * The source image cannot use an IndexColorModel, and the destination image
176       * (if one is provided) must have the same size.
177       *
178       * If the final value of a sample is beyond the range of the color model, it
179       * will be clipped to the appropriate maximum / minimum.
180       *
181       * @param src The source image.
182       * @param dst The destination image.
183       * @throws IllegalArgumentException if the rasters and/or color spaces are
184       *            incompatible.
185       * @return The rescaled image.
186       */
187      public final BufferedImage filter(BufferedImage src, BufferedImage dst)
188      {
189        // Initial checks
190        if (scale.length != 1
191            && scale.length != src.getColorModel().getNumComponents()
192            && (scale.length != src.getColorModel().getNumColorComponents()))
193          throw new IllegalArgumentException("Source image has wrong number of "
194                                             + "bands for these scaling factors.");
195    
196        if (dst == null)
197          dst = createCompatibleDestImage(src, null);
198        else if (src.getHeight() != dst.getHeight()
199                 || src.getWidth() != dst.getWidth())
200          throw new IllegalArgumentException("Source and destination images are "
201                                             + "different sizes.");
202    
203        // Prepare for possible colorspace conversion
204        BufferedImage dst2 = dst;
205        if (dst.getColorModel().getColorSpace().getType() != src.getColorModel().getColorSpace().getType())
206          dst2 = createCompatibleDestImage(src, src.getColorModel());
207    
208        // Figure out how many bands to scale
209        int numBands = scale.length;
210        if (scale.length == 1)
211          numBands = src.getColorModel().getNumColorComponents();
212        boolean[] bands = new boolean[numBands];
213        // this assumes the alpha, if present, is the last band
214        Arrays.fill(bands, true);
215    
216        // Perform rescaling
217        filter(src.getRaster(), dst2.getRaster(), bands);
218    
219        // Copy alpha band if needed (ie if it exists and wasn't scaled)
220        // NOTE: This assumes the alpha component is the last band!
221        if (src.getColorModel().hasAlpha()
222            && numBands == src.getColorModel().getNumColorComponents())
223          {
224    
225            dst2.getRaster().setSamples(0, 0, src.getWidth(), src.getHeight(),
226                                        numBands,
227                                        src.getRaster().getSamples(0, 0,
228                                                                   src.getWidth(),
229                                                                   src.getHeight(),
230                                                                   numBands,
231                                                                   (int[]) null));
232          }
233    
234        // Perform colorspace conversion if needed
235        if (dst != dst2)
236          new ColorConvertOp(hints).filter(dst2, dst);
237    
238        return dst;
239      }
240    
241      /* (non-Javadoc)
242       * @see java.awt.image.RasterOp#filter(java.awt.image.Raster, java.awt.image.WritableRaster)
243       */
244      public final WritableRaster filter(Raster src, WritableRaster dest)
245      {
246        // Required sanity checks
247        if (scale.length != 1 && scale.length != src.numBands)
248          throw new IllegalArgumentException("Number of rasters is incompatible "
249                                                 + "with the number of scaling "
250                                                 + "factors provided.");
251    
252        if (dest == null)
253          dest = src.createCompatibleWritableRaster();
254        else if (src.getHeight() != dest.getHeight()
255                 || src.getWidth() != dest.getWidth())
256          throw new IllegalArgumentException("Source and destination rasters are "
257                                             + "different sizes.");
258        else if (src.numBands != dest.numBands)
259          throw new IllegalArgumentException("Source and destination rasters "
260                                             + "are incompatible.");
261    
262        // Filter all bands
263        boolean[] bands = new boolean[src.getNumBands()];
264        Arrays.fill(bands, true);
265        return filter(src, dest, bands);
266      }
267    
268      /**
269       * Perform raster-based filtering on a selected number of bands.
270       *
271       * The length of the bands array should equal the number of bands; a true
272       * element indicates filtering should happen on the corresponding band, while
273       * a false element will skip the band.
274       *
275       * The rasters are assumed to be compatible and non-null.
276       *
277       * @param src the source raster.
278       * @param dest the destination raster.
279       * @param bands an array indicating which bands to filter.
280       * @throws NullPointerException if any parameter is null.
281       * @throws ArrayIndexOutOfBoundsException if the bands array is too small.
282       * @return the destination raster.
283       */
284      private WritableRaster filter(Raster src, WritableRaster dest, boolean[] bands)
285      {
286        int[] values = new int[src.getHeight() * src.getWidth()];
287        float scaleFactor, offset;
288    
289        // Find max sample value, to be used for clipping later
290        int[] maxValue = src.getSampleModel().getSampleSize();
291        for (int i = 0; i < maxValue.length; i++)
292          maxValue[i] = (int)Math.pow(2, maxValue[i]) - 1;
293    
294        // TODO: can this be optimized further?
295        // Filter all samples of all requested bands
296        for (int band = 0; band < bands.length; band++)
297          if (bands[band])
298            {
299              values = src.getSamples(src.getMinX(), src.getMinY(), src.getWidth(),
300                                      src.getHeight(), band, values);
301    
302              if (scale.length == 1)
303                {
304                  scaleFactor = scale[0];
305                  offset = offsets[0];
306                }
307              else
308                {
309                  scaleFactor = scale[band];
310                  offset = offsets[band];
311                }
312    
313              for (int i = 0; i < values.length; i++)
314                {
315                  values[i] = (int) (values[i] * scaleFactor + offset);
316    
317                  // Clip if needed
318                  if (values[i] < 0)
319                    values[i] = 0;
320                  if (values[i] > maxValue[band])
321                    values[i] = maxValue[band];
322                }
323    
324              dest.setSamples(dest.getMinX(), dest.getMinY(), dest.getWidth(),
325                              dest.getHeight(), band, values);
326            }
327    
328        return dest;
329      }
330    
331      /*
332       * (non-Javadoc)
333       *
334       * @see java.awt.image.BufferedImageOp#createCompatibleDestImage(java.awt.image.BufferedImage,
335       *      java.awt.image.ColorModel)
336       */
337      public BufferedImage createCompatibleDestImage(BufferedImage src,
338                                                     ColorModel dstCM)
339      {
340        if (dstCM == null)
341          return new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
342    
343        return new BufferedImage(dstCM,
344                                 src.getRaster().createCompatibleWritableRaster(),
345                                 src.isAlphaPremultiplied(), null);
346      }
347    
348      /* (non-Javadoc)
349       * @see java.awt.image.RasterOp#createCompatibleDestRaster(java.awt.image.Raster)
350       */
351      public WritableRaster createCompatibleDestRaster(Raster src)
352      {
353        return src.createCompatibleWritableRaster();
354      }
355    
356      /* (non-Javadoc)
357       * @see java.awt.image.BufferedImageOp#getBounds2D(java.awt.image.BufferedImage)
358       */
359      public final Rectangle2D getBounds2D(BufferedImage src)
360      {
361        return src.getRaster().getBounds();
362      }
363    
364      /* (non-Javadoc)
365       * @see java.awt.image.RasterOp#getBounds2D(java.awt.image.Raster)
366       */
367      public final Rectangle2D getBounds2D(Raster src)
368      {
369        return src.getBounds();
370      }
371    
372      /* (non-Javadoc)
373       * @see java.awt.image.BufferedImageOp#getPoint2D(java.awt.geom.Point2D, java.awt.geom.Point2D)
374       */
375      public final Point2D getPoint2D(Point2D src, Point2D dst)
376      {
377        if (dst == null)
378          dst = (Point2D) src.clone();
379        else
380          dst.setLocation(src);
381    
382        return dst;
383      }
384    
385    }