001/* ConvolveOp.java --
002   Copyright (C) 2004, 2005, 2006, Free Software Foundation -- ConvolveOp
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package java.awt.image;
040
041import java.awt.RenderingHints;
042import java.awt.geom.Point2D;
043import java.awt.geom.Rectangle2D;
044
045/**
046 * Convolution filter.
047 * 
048 * ConvolveOp convolves the source image with a Kernel to generate a
049 * destination image.  This involves multiplying each pixel and its neighbors
050 * with elements in the kernel to compute a new pixel.
051 * 
052 * Each band in a Raster is convolved and copied to the destination Raster.
053 * For BufferedImages, convolution is applied to all components.  Color 
054 * conversion will be applied if needed.
055 * 
056 * Note that this filter ignores whether the source or destination is alpha
057 * premultiplied.  The reference spec states that data will be premultiplied
058 * prior to convolving and divided back out afterwards (if needed), but testing
059 * has shown that this is not the case with their implementation.
060 * 
061 * @author jlquinn@optonline.net
062 */
063public class ConvolveOp implements BufferedImageOp, RasterOp
064{
065  /** Edge pixels are set to 0. */
066  public static final int EDGE_ZERO_FILL = 0;
067  
068  /** Edge pixels are copied from the source. */
069  public static final int EDGE_NO_OP = 1;
070  
071  private Kernel kernel;
072  private int edge;
073  private RenderingHints hints;
074
075  /**
076   * Construct a ConvolveOp.
077   * 
078   * The edge condition specifies that pixels outside the area that can be
079   * filtered are either set to 0 or copied from the source image.
080   * 
081   * @param kernel The kernel to convolve with.
082   * @param edgeCondition Either EDGE_ZERO_FILL or EDGE_NO_OP.
083   * @param hints Rendering hints for color conversion, or null.
084   */
085  public ConvolveOp(Kernel kernel,
086                                int edgeCondition,
087                                RenderingHints hints)
088  {
089    this.kernel = kernel;
090    edge = edgeCondition;
091    this.hints = hints;
092  }
093  
094  /**
095   * Construct a ConvolveOp.
096   * 
097   * The edge condition defaults to EDGE_ZERO_FILL.
098   * 
099   * @param kernel The kernel to convolve with.
100   */
101  public ConvolveOp(Kernel kernel)
102  {
103    this.kernel = kernel;
104    edge = EDGE_ZERO_FILL;
105    hints = null;
106  }
107
108  /**
109   * Converts the source image using the kernel specified in the
110   * constructor.  The resulting image is stored in the destination image if one
111   * is provided; otherwise a new BufferedImage is created and returned. 
112   * 
113   * The source and destination BufferedImage (if one is supplied) must have
114   * the same dimensions.
115   *
116   * @param src The source image.
117   * @param dst The destination image.
118   * @throws IllegalArgumentException if the rasters and/or color spaces are
119   *            incompatible.
120   * @return The convolved image.
121   */
122  public final BufferedImage filter(BufferedImage src, BufferedImage dst)
123  {
124    if (src == dst)
125      throw new IllegalArgumentException("Source and destination images " +
126            "cannot be the same.");
127    
128    if (dst == null)
129      dst = createCompatibleDestImage(src, src.getColorModel());
130    
131    // Make sure source image is premultiplied
132    BufferedImage src1 = src;
133    // The spec says we should do this, but mauve testing shows that Sun's
134    // implementation does not check this.
135    /*
136    if (!src.isAlphaPremultiplied())
137    {
138      src1 = createCompatibleDestImage(src, src.getColorModel());
139      src.copyData(src1.getRaster());
140      src1.coerceData(true);
141    }
142    */
143
144    BufferedImage dst1 = dst;
145    if (src1.getColorModel().getColorSpace().getType() != dst.getColorModel().getColorSpace().getType())
146      dst1 = createCompatibleDestImage(src, src.getColorModel());
147
148    filter(src1.getRaster(), dst1.getRaster());
149    
150    // Since we don't coerceData above, we don't need to divide it back out.
151    // This is wrong (one mauve test specifically tests converting a non-
152    // premultiplied image to a premultiplied image, and it shows that Sun
153    // simply ignores the premultipled flag, contrary to the spec), but we
154    // mimic it for compatibility.
155    /*
156        if (! dst.isAlphaPremultiplied())
157          dst1.coerceData(false);
158    */
159
160    // Convert between color models if needed
161    if (dst1 != dst)
162      new ColorConvertOp(hints).filter(dst1, dst);
163
164    return dst;
165  }
166
167  /**
168   * Creates an empty BufferedImage with the size equal to the source and the
169   * correct number of bands. The new image is created with the specified 
170   * ColorModel, or if no ColorModel is supplied, an appropriate one is chosen.
171   *
172   * @param src The source image.
173   * @param dstCM A color model for the destination image (may be null).
174   * @return The new compatible destination image.
175   */
176  public BufferedImage createCompatibleDestImage(BufferedImage src,
177                                                 ColorModel dstCM)
178  {
179    if (dstCM != null)
180      return new BufferedImage(dstCM,
181                               src.getRaster().createCompatibleWritableRaster(),
182                               src.isAlphaPremultiplied(), null);
183
184    return new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
185  }
186
187  /* (non-Javadoc)
188   * @see java.awt.image.RasterOp#getRenderingHints()
189   */
190  public final RenderingHints getRenderingHints()
191  {
192    return hints;
193  }
194  
195  /**
196   * Get the edge condition for this Op.
197   * 
198   * @return The edge condition.
199   */
200  public int getEdgeCondition()
201  {
202    return edge;
203  }
204  
205  /**
206   * Returns (a clone of) the convolution kernel.
207   *
208   * @return The convolution kernel.
209   */
210  public final Kernel getKernel()
211  {
212    return (Kernel) kernel.clone();
213  }
214
215  /**
216   * Converts the source raster using the kernel specified in the constructor.  
217   * The resulting raster is stored in the destination raster if one is 
218   * provided; otherwise a new WritableRaster is created and returned.
219   * 
220   * If the convolved value for a sample is outside the range of [0-255], it
221   * will be clipped.
222   * 
223   * The source and destination raster (if one is supplied) cannot be the same,
224   * and must also have the same dimensions.
225   *
226   * @param src The source raster.
227   * @param dest The destination raster.
228   * @throws IllegalArgumentException if the rasters identical.
229   * @throws ImagingOpException if the convolution is not possible.
230   * @return The transformed raster.
231   */
232  public final WritableRaster filter(Raster src, WritableRaster dest)
233  {
234    if (src == dest)
235      throw new IllegalArgumentException("src == dest is not allowed.");
236    if (kernel.getWidth() > src.getWidth() 
237        || kernel.getHeight() > src.getHeight())
238      throw new ImagingOpException("The kernel is too large.");
239    if (dest == null)
240      dest = createCompatibleDestRaster(src);
241    else if (src.getNumBands() != dest.getNumBands())
242      throw new ImagingOpException("src and dest have different band counts.");
243
244    // calculate the borders that the op can't reach...
245    int kWidth = kernel.getWidth();
246    int kHeight = kernel.getHeight();
247    int left = kernel.getXOrigin();
248    int right = Math.max(kWidth - left - 1, 0);
249    int top = kernel.getYOrigin();
250    int bottom = Math.max(kHeight - top - 1, 0);
251    
252    // Calculate max sample values for clipping
253    int[] maxValue = src.getSampleModel().getSampleSize();
254    for (int i = 0; i < maxValue.length; i++)
255      maxValue[i] = (int)Math.pow(2, maxValue[i]) - 1;
256    
257    // process the region that is reachable...
258    int regionW = src.width - left - right;
259    int regionH = src.height - top - bottom;
260    float[] kvals = kernel.getKernelData(null);
261    float[] tmp = new float[kWidth * kHeight];
262
263    for (int x = 0; x < regionW; x++)
264      {
265        for (int y = 0; y < regionH; y++)
266          {
267            // FIXME: This needs a much more efficient implementation
268            for (int b = 0; b < src.getNumBands(); b++)
269            {
270              float v = 0;
271              src.getSamples(x, y, kWidth, kHeight, b, tmp);
272              for (int i = 0; i < tmp.length; i++)
273                v += tmp[tmp.length - i - 1] * kvals[i];
274                // FIXME: in the above line, I've had to reverse the order of 
275                // the samples array to make the tests pass.  I haven't worked 
276                // out why this is necessary.
277
278              // This clipping is is undocumented, but determined by testing.
279              if (v > maxValue[b])
280                v = maxValue[b];
281              else if (v < 0)
282                v = 0;
283
284              dest.setSample(x + kernel.getXOrigin(), y + kernel.getYOrigin(), 
285                             b, v);
286            }
287          }
288      }
289    
290    // fill in the top border
291    fillEdge(src, dest, 0, 0, src.width, top, edge);
292    
293    // fill in the bottom border
294    fillEdge(src, dest, 0, src.height - bottom, src.width, bottom, edge);
295    
296    // fill in the left border
297    fillEdge(src, dest, 0, top, left, regionH, edge);
298    
299    // fill in the right border
300    fillEdge(src, dest, src.width - right, top, right, regionH, edge);
301    
302    return dest;  
303  }
304  
305  /**
306   * Fills a range of pixels (typically at the edge of a raster) with either
307   * zero values (if <code>edgeOp</code> is <code>EDGE_ZERO_FILL</code>) or the 
308   * corresponding pixel values from the source raster (if <code>edgeOp</code>
309   * is <code>EDGE_NO_OP</code>).  This utility method is called by the 
310   * {@link #fillEdge(Raster, WritableRaster, int, int, int, int, int)} method.
311   * 
312   * @param src  the source raster.
313   * @param dest  the destination raster.
314   * @param x  the x-coordinate of the top left pixel in the range.
315   * @param y  the y-coordinate of the top left pixel in the range.
316   * @param w  the width of the pixel range.
317   * @param h  the height of the pixel range.
318   * @param edgeOp  indicates how to determine the values for the range
319   *     (either {@link #EDGE_ZERO_FILL} or {@link #EDGE_NO_OP}).
320   */
321  private void fillEdge(Raster src, WritableRaster dest, int x, int y, int w, 
322                        int h, int edgeOp) 
323  {
324    if (w <= 0)
325      return;
326    if (h <= 0)
327      return;
328    if (edgeOp == EDGE_ZERO_FILL)  // fill region with zeroes
329      {
330        float[] zeros = new float[src.getNumBands() * w * h];
331        dest.setPixels(x, y, w, h, zeros); 
332      }
333    else  // copy pixels from source
334      {
335        float[] pixels = new float[src.getNumBands() * w * h];
336        src.getPixels(x, y, w, h, pixels);
337        dest.setPixels(x, y, w, h, pixels);
338      }
339  }
340
341  /* (non-Javadoc)
342   * @see java.awt.image.RasterOp#createCompatibleDestRaster(java.awt.image.Raster)
343   */
344  public WritableRaster createCompatibleDestRaster(Raster src)
345  {
346    return src.createCompatibleWritableRaster();
347  }
348
349  /* (non-Javadoc)
350   * @see java.awt.image.BufferedImageOp#getBounds2D(java.awt.image.BufferedImage)
351   */
352  public final Rectangle2D getBounds2D(BufferedImage src)
353  {
354    return src.getRaster().getBounds();
355  }
356
357  /* (non-Javadoc)
358   * @see java.awt.image.RasterOp#getBounds2D(java.awt.image.Raster)
359   */
360  public final Rectangle2D getBounds2D(Raster src)
361  {
362    return src.getBounds();
363  }
364
365  /**
366   * Returns the corresponding destination point for a source point. Because
367   * this is not a geometric operation, the destination and source points will
368   * be identical.
369   * 
370   * @param src The source point.
371   * @param dst The transformed destination point.
372   * @return The transformed destination point.
373   */
374  public final Point2D getPoint2D(Point2D src, Point2D dst)
375  {
376    if (dst == null) return (Point2D)src.clone();
377    dst.setLocation(src);
378    return dst;
379  }
380}