001/* AffineTransformOp.java --  This class performs affine 
002   transformation between two images or rasters in 2 dimensions.
003   Copyright (C) 2004, 2006 Free Software Foundation
004
005This file is part of GNU Classpath.
006
007GNU Classpath is free software; you can redistribute it and/or modify
008it under the terms of the GNU General Public License as published by
009the Free Software Foundation; either version 2, or (at your option)
010any later version.
011
012GNU Classpath is distributed in the hope that it will be useful, but
013WITHOUT ANY WARRANTY; without even the implied warranty of
014MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015General Public License for more details.
016
017You should have received a copy of the GNU General Public License
018along with GNU Classpath; see the file COPYING.  If not, write to the
019Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02002110-1301 USA.
021
022Linking this library statically or dynamically with other modules is
023making a combined work based on this library.  Thus, the terms and
024conditions of the GNU General Public License cover the whole
025combination.
026
027As a special exception, the copyright holders of this library give you
028permission to link this library with independent modules to produce an
029executable, regardless of the license terms of these independent
030modules, and to copy and distribute the resulting executable under
031terms of your choice, provided that you also meet, for each linked
032independent module, the terms and conditions of the license of that
033module.  An independent module is a module which is not derived from
034or based on this library.  If you modify this library, you may extend
035this exception to your version of the library, but you are not
036obligated to do so.  If you do not wish to do so, delete this
037exception statement from your version. */
038
039package java.awt.image;
040
041import java.awt.Graphics2D;
042import java.awt.Point;
043import java.awt.Rectangle;
044import java.awt.RenderingHints;
045import java.awt.geom.AffineTransform;
046import java.awt.geom.NoninvertibleTransformException;
047import java.awt.geom.Point2D;
048import java.awt.geom.Rectangle2D;
049import java.util.Arrays;
050
051/**
052 * AffineTransformOp performs matrix-based transformations (translations,
053 * scales, flips, rotations, and shears).
054 * 
055 * If interpolation is required, nearest neighbour, bilinear, and bicubic
056 * methods are available.
057 *
058 * @author Olga Rodimina (rodimina@redhat.com) 
059 * @author Francis Kung (fkung@redhat.com)
060 */
061public class AffineTransformOp implements BufferedImageOp, RasterOp
062{
063    public static final int TYPE_NEAREST_NEIGHBOR = 1;
064    
065    public static final int TYPE_BILINEAR = 2;
066    
067    /**
068     * @since 1.5.0
069     */
070    public static final int TYPE_BICUBIC = 3;
071
072    private AffineTransform transform;
073    private RenderingHints hints;
074    
075    /**
076     * Construct AffineTransformOp with the given xform and interpolationType.
077     * Interpolation type can be TYPE_BILINEAR, TYPE_BICUBIC or
078     * TYPE_NEAREST_NEIGHBOR.
079     *
080     * @param xform AffineTransform that will applied to the source image 
081     * @param interpolationType type of interpolation used
082     * @throws ImagingOpException if the transform matrix is noninvertible
083     */
084    public AffineTransformOp (AffineTransform xform, int interpolationType)
085    {
086      this.transform = xform;
087      if (xform.getDeterminant() == 0)
088        throw new ImagingOpException(null);
089
090      switch (interpolationType)
091      {
092      case TYPE_BILINEAR:
093        hints = new RenderingHints (RenderingHints.KEY_INTERPOLATION, 
094                                    RenderingHints.VALUE_INTERPOLATION_BILINEAR);
095        break;
096      case TYPE_BICUBIC:
097        hints = new RenderingHints (RenderingHints.KEY_INTERPOLATION, 
098                                    RenderingHints.VALUE_INTERPOLATION_BICUBIC);
099        break;
100      default:
101        hints = new RenderingHints (RenderingHints.KEY_INTERPOLATION,
102                                    RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
103      }
104    }
105
106    /**
107     * Construct AffineTransformOp with the given xform and rendering hints.
108     * 
109     * @param xform AffineTransform that will applied to the source image
110     * @param hints rendering hints that will be used during transformation
111     * @throws ImagingOpException if the transform matrix is noninvertible
112     */
113    public AffineTransformOp (AffineTransform xform, RenderingHints hints)
114    {
115      this.transform = xform;
116      this.hints = hints;
117      if (xform.getDeterminant() == 0)
118        throw new ImagingOpException(null);
119    }
120
121    /**
122     * Creates a new BufferedImage with the size equal to that of the 
123     * transformed image and the correct number of bands. The newly created 
124     * image is created with the specified ColorModel. 
125     * If a ColorModel is not specified, an appropriate ColorModel is used.
126     *
127     * @param src the source image.
128     * @param destCM color model for the destination image (can be null).
129     * @return a new compatible destination image.
130     */
131    public BufferedImage createCompatibleDestImage (BufferedImage src,
132                                                    ColorModel destCM)
133    {
134      if (destCM != null)
135        return new BufferedImage(destCM,
136                                 createCompatibleDestRaster(src.getRaster()),
137                                 src.isAlphaPremultiplied(), null);
138
139      // This behaviour was determined by Mauve testcases, and is compatible
140      // with the reference implementation
141      if (src.getType() == BufferedImage.TYPE_INT_ARGB_PRE
142          || src.getType() == BufferedImage.TYPE_4BYTE_ABGR
143          || src.getType() == BufferedImage.TYPE_4BYTE_ABGR_PRE)
144        return new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
145
146      else
147        return new BufferedImage(src.getWidth(), src.getHeight(),
148                                 BufferedImage.TYPE_INT_ARGB);
149    }
150
151    /**
152     * Creates a new WritableRaster with the size equal to the transformed 
153     * source raster and correct number of bands .
154     *
155     * @param src the source raster.
156     * @throws RasterFormatException if resulting width or height of raster is 0.
157     * @return a new compatible raster.
158     */
159    public WritableRaster createCompatibleDestRaster (Raster src)
160    {
161      Rectangle2D rect = getBounds2D(src);
162      
163      if (rect.getWidth() == 0 || rect.getHeight() == 0) 
164        throw new RasterFormatException("width or height is 0");
165
166      return src.createCompatibleWritableRaster((int) rect.getWidth(), 
167                                                (int) rect.getHeight());
168    }
169
170    /**
171     * Transforms source image using transform specified at the constructor.
172     * The resulting transformed image is stored in the destination image if one
173     * is provided; otherwise a new BufferedImage is created and returned. 
174     *
175     * @param src source image
176     * @param dst destination image
177     * @throws IllegalArgumentException if the source and destination image are
178     *          the same
179     * @return transformed source image.
180     */
181    public final BufferedImage filter (BufferedImage src, BufferedImage dst)
182    {
183      if (dst == src)
184        throw new IllegalArgumentException("src image cannot be the same as "
185                                         + "the dst image");
186
187      // If the destination image is null, then use a compatible BufferedImage
188      if (dst == null)
189        dst = createCompatibleDestImage(src, null);
190
191      Graphics2D gr = dst.createGraphics();
192      gr.setRenderingHints(hints);
193      gr.drawImage(src, transform, null);
194      return dst;
195    }
196
197    /**
198     * Transforms source raster using transform specified at the constructor.
199     * The resulting raster is stored in the destination raster if it is not
200     * null, otherwise a new raster is created and returned.
201     *
202     * @param src source raster
203     * @param dst destination raster
204     * @throws IllegalArgumentException if the source and destination are not
205     *          compatible
206     * @return transformed raster.
207     */
208    public final WritableRaster filter(Raster src, WritableRaster dst)
209    {
210      // Initial checks
211      if (dst == src)
212        throw new IllegalArgumentException("src image cannot be the same as"
213                                           + " the dst image");
214
215      if (dst == null)
216        dst = createCompatibleDestRaster(src);
217
218      if (src.getNumBands() != dst.getNumBands())
219        throw new IllegalArgumentException("src and dst must have same number"
220                                           + " of bands");
221      
222      // Optimization for rasters that can be represented in the RGB colormodel:
223      // wrap the rasters in images, and let Cairo do the transformation
224      if (ColorModel.getRGBdefault().isCompatibleSampleModel(src.getSampleModel())
225          && ColorModel.getRGBdefault().isCompatibleSampleModel(dst.getSampleModel()))
226        {
227          WritableRaster src2 = Raster.createWritableRaster(src.getSampleModel(),
228                                                            src.getDataBuffer(),
229                                                            new Point(src.getMinX(),
230                                                                      src.getMinY()));
231          BufferedImage iSrc = new BufferedImage(ColorModel.getRGBdefault(),
232                                                 src2, false, null);
233          BufferedImage iDst = new BufferedImage(ColorModel.getRGBdefault(), dst,
234                                                 false, null);
235  
236          return filter(iSrc, iDst).getRaster();
237        }
238
239      // Otherwise, we need to do the transformation in java code...
240      // Create arrays to hold all the points
241      double[] dstPts = new double[dst.getHeight() * dst.getWidth() * 2];
242      double[] srcPts = new double[dst.getHeight() * dst.getWidth() * 2];
243
244      // Populate array with all points in the *destination* raster
245      int i = 0;
246      for (int x = 0; x < dst.getWidth(); x++)
247        {
248          for (int y = 0; y < dst.getHeight(); y++)
249            {
250              dstPts[i++] = x;
251              dstPts[i++] = y;
252            }
253        }
254      Rectangle srcbounds = src.getBounds();
255
256      // Use an inverse transform to map each point in the destination to
257      // a point in the source.  Note that, while all points in the destination
258      // matrix are integers, this is not necessarily true for points in the
259      // source (hence why interpolation is required) 
260      try
261        {
262          AffineTransform inverseTx = transform.createInverse();
263          inverseTx.transform(dstPts, 0, srcPts, 0, dstPts.length / 2);
264        }
265      catch (NoninvertibleTransformException e)
266        {
267          // Shouldn't happen since the constructor traps this
268          throw new ImagingOpException(e.getMessage());
269        }
270
271      // Different interpolation methods...
272      if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR))
273        filterNearest(src, dst, dstPts, srcPts);
274      
275      else if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_BILINEAR))
276        filterBilinear(src, dst, dstPts, srcPts);
277    
278      else          // bicubic
279        filterBicubic(src, dst, dstPts, srcPts);
280
281      return dst;  
282    }
283
284    /**
285     * Transforms source image using transform specified at the constructor and 
286     * returns bounds of the transformed image.
287     *
288     * @param src image to be transformed
289     * @return bounds of the transformed image.
290     */
291    public final Rectangle2D getBounds2D (BufferedImage src)
292    {
293      return getBounds2D (src.getRaster());
294    }
295   
296    /**
297     * Returns bounds of the transformed raster.
298     *
299     * @param src raster to be transformed
300     * @return bounds of the transformed raster.
301     */
302    public final Rectangle2D getBounds2D (Raster src)
303    {
304      return transform.createTransformedShape(src.getBounds()).getBounds2D();
305    }
306
307    /**
308     * Returns interpolation type used during transformations.
309     *
310     * @return interpolation type
311     */
312    public final int getInterpolationType ()
313    {
314      if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_BILINEAR))
315        return TYPE_BILINEAR;
316      
317      else if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_BICUBIC))
318        return TYPE_BICUBIC;
319      
320      else 
321        return TYPE_NEAREST_NEIGHBOR;
322    }
323
324    /** 
325     * Returns location of the transformed source point. The resulting point 
326     * is stored in the dstPt if one is specified.
327     *  
328     * @param srcPt point to be transformed
329     * @param dstPt destination point
330     * @return the location of the transformed source point.
331     */
332    public final Point2D getPoint2D (Point2D srcPt, Point2D dstPt)
333    {
334      return transform.transform (srcPt, dstPt);
335    }
336
337    /**
338     * Returns rendering hints that are used during transformation.
339     *
340     * @return the rendering hints used in this Op.
341     */
342    public final RenderingHints getRenderingHints ()
343    {
344      return hints;
345    }
346
347    /**
348     * Returns transform used in transformation between source and destination
349     * image.
350     *
351     * @return the transform used in this Op.
352     */
353    public final AffineTransform getTransform ()
354    {
355      return transform;
356    }
357    
358    /**
359     * Perform nearest-neighbour filtering
360     * 
361     * @param src the source raster
362     * @param dst the destination raster
363     * @param dpts array of points on the destination raster
364     * @param pts array of corresponding points on the source raster
365     */
366    private void filterNearest(Raster src, WritableRaster dst, double[] dpts,
367                               double[] pts)
368    {
369      Rectangle srcbounds = src.getBounds();
370  
371      // For all points on the destination raster, copy the value from the
372      // corrosponding (rounded) source point
373      for (int i = 0; i < dpts.length; i += 2)
374        {
375          int srcX = (int) Math.round(pts[i]) + src.getMinX();
376          int srcY = (int) Math.round(pts[i + 1]) + src.getMinY();
377          
378          if (srcbounds.contains(srcX, srcY))
379            dst.setDataElements((int) dpts[i] + dst.getMinX(),
380                                (int) dpts[i + 1] + dst.getMinY(),
381                                src.getDataElements(srcX, srcY, null));
382        }
383    }
384
385    /**
386     * Perform bilinear filtering
387     * 
388     * @param src the source raster
389     * @param dst the destination raster
390     * @param dpts array of points on the destination raster
391     * @param pts array of corresponding points on the source raster
392     */
393    private void filterBilinear(Raster src, WritableRaster dst, double[] dpts,
394                              double[] pts)
395    {
396      Rectangle srcbounds = src.getBounds();
397  
398      Object xyarr = null;
399      Object xp1arr = null;
400      Object yp1arr = null;
401      Object xyp1arr = null;
402      
403      double xy;
404      double xp1;
405      double yp1;
406      double xyp1;
407
408      double[] result = new double[src.getNumBands()];
409      
410      // For all points in the destination raster, use bilinear interpolation
411      // to find the value from the corrosponding source points
412      for (int i = 0; i < dpts.length; i += 2)
413        {
414          int srcX = (int) Math.round(pts[i]) + src.getMinX();
415          int srcY = (int) Math.round(pts[i + 1]) + src.getMinY();
416          
417          if (srcbounds.contains(srcX, srcY))
418            {
419              // Corner case at the bottom or right edge; use nearest neighbour
420              if (pts[i] >= src.getWidth() - 1
421                  || pts[i + 1] >= src.getHeight() - 1)
422                dst.setDataElements((int) dpts[i] + dst.getMinX(),
423                                    (int) dpts[i + 1] + dst.getMinY(),
424                                    src.getDataElements(srcX, srcY, null));
425  
426              // Standard case, apply the bilinear formula
427              else
428                {
429                  int x = (int) Math.floor(pts[i] + src.getMinX());
430                  int y = (int) Math.floor(pts[i + 1] + src.getMinY());
431                  double xdiff = pts[i] + src.getMinX() - x;
432                  double ydiff = pts[i + 1] + src.getMinY() - y;
433
434                  // Get surrounding pixels used in interpolation... optimized
435                  // to use the smallest datatype possible.
436                  if (src.getTransferType() == DataBuffer.TYPE_DOUBLE
437                      || src.getTransferType() == DataBuffer.TYPE_FLOAT)
438                    {
439                      xyarr = src.getPixel(x, y, (double[])xyarr);
440                      xp1arr  = src.getPixel(x+1, y, (double[])xp1arr);
441                      yp1arr = src.getPixel(x, y+1, (double[])yp1arr);
442                      xyp1arr = src.getPixel(x+1, y+1, (double[])xyp1arr);
443                    }
444                  else
445                    {
446                      xyarr = src.getPixel(x, y, (int[])xyarr);
447                      xp1arr  = src.getPixel(x+1, y, (int[])xp1arr);
448                      yp1arr = src.getPixel(x, y+1, (int[])yp1arr);
449                      xyp1arr = src.getPixel(x+1, y+1, (int[])xyp1arr);
450                    }
451                  // using 
452                  // array[] pixels = src.getPixels(x, y, 2, 2, pixels);
453                  // instead of doing four individual src.getPixel() calls
454                  // should be faster, but benchmarking shows that it's not...
455                  
456                  // Run interpolation for each band
457                  for (int j = 0; j < src.getNumBands(); j++)
458                    {
459                      // Pull individual sample values out of array
460                      if (src.getTransferType() == DataBuffer.TYPE_DOUBLE
461                          || src.getTransferType() == DataBuffer.TYPE_FLOAT)
462                        {
463                          xy = ((double[])xyarr)[j];
464                          xp1  = ((double[])xp1arr)[j];
465                          yp1 = ((double[])yp1arr)[j];
466                          xyp1 = ((double[])xyp1arr)[j];
467                        }
468                      else
469                        {
470                          xy = ((int[])xyarr)[j];
471                          xp1  = ((int[])xp1arr)[j];
472                          yp1 = ((int[])yp1arr)[j];
473                          xyp1 = ((int[])xyp1arr)[j];
474                        }
475                      
476                      // If all four samples are identical, there's no need to 
477                      // calculate anything
478                      if (xy == xp1 && xy == yp1 && xy == xyp1)
479                        result[j] = xy;
480                      
481                      // Run bilinear interpolation formula
482                      else
483                        result[j] = (xy * (1-xdiff) + xp1 * xdiff) 
484                                      * (1-ydiff) 
485                                    + (yp1 * (1-xdiff) + xyp1 * xdiff)
486                                      * ydiff;
487                    }
488
489                  dst.setPixel((int)dpts[i] + dst.getMinX(),
490                               (int)dpts[i+1] + dst.getMinY(),
491                               result);
492                }
493            }
494        }
495    }
496
497    /**
498     * Perform bicubic filtering
499     * based on http://local.wasp.uwa.edu.au/~pbourke/colour/bicubic/
500     * 
501     * @param src the source raster
502     * @param dst the destination raster
503     * @param dpts array of points on the destination raster
504     * @param pts array of corresponding points on the source raster
505     */
506    private void filterBicubic(Raster src, WritableRaster dst, double[] dpts,
507                               double[] pts)
508    {
509      Rectangle srcbounds = src.getBounds();
510      double[] result = new double[src.getNumBands()];
511      Object pixels = null;
512
513      // For all points on the destination raster, perform bicubic interpolation
514      // from corrosponding source points
515      for (int i = 0; i < dpts.length; i += 2)
516        {
517          if (srcbounds.contains((int) Math.round(pts[i]) + src.getMinX(),
518                                 (int) Math.round(pts[i + 1]) + src.getMinY()))
519            {
520              int x = (int) Math.floor(pts[i] + src.getMinX());
521              int y = (int) Math.floor(pts[i + 1] + src.getMinY());
522              double dx = pts[i] + src.getMinX() - x;
523              double dy = pts[i + 1] + src.getMinY() - y;
524              Arrays.fill(result, 0);
525  
526              for (int m = - 1; m < 3; m++)
527                for (int n = - 1; n < 3; n++)
528                  {
529                    // R(x) = ( P(x+2)^3 - 4 P(x+1)^3 + 6 P(x)^3 - 4 P(x-1)^3 ) / 6
530                    double r1 = 0;
531                    double r2 = 0;
532
533                    // Calculate R(m - dx)
534                    double rx = m - dx + 2;
535                    r1 += rx * rx * rx;
536
537                    rx = m - dx + 1;
538                    if (rx > 0)
539                      r1 -= 4 * rx * rx * rx;
540
541                    rx = m - dx;
542                    if (rx > 0)
543                      r1 += 6 * rx * rx * rx;
544
545                    rx = m - dx - 1;
546                    if (rx > 0)
547                      r1 -= 4 * rx * rx * rx;
548
549                    r1 /= 6;
550
551                    // Calculate R(dy - n);
552                    rx = dy - n + 2;
553                    if (rx > 0)
554                      r2 += rx * rx * rx;
555
556                    rx = dy - n + 1;
557                    if (rx > 0)
558                      r2 -= 4 * rx * rx * rx;
559
560                    rx = dy - n;
561                    if (rx > 0)
562                      r2 += 6 * rx * rx * rx;
563
564                    rx = dy - n - 1;
565                    if (rx > 0)
566                      r2 -= 4 * rx * rx * rx;
567
568                    r2 /= 6;
569
570                    // Calculate F(i+m, j+n) R(m - dx) R(dy - n)
571                    // Check corner cases
572                    int srcX = x + m;
573                    if (srcX >= src.getMinX() + src.getWidth())
574                      srcX = src.getMinX() + src.getWidth() - 1;
575                    else if (srcX < src.getMinX())
576                      srcX = src.getMinX();
577
578                    int srcY = y + n;
579                    if (srcY >= src.getMinY() + src.getHeight())
580                      srcY = src.getMinY() + src.getHeight() - 1;
581                    else if (srcY < src.getMinY())
582                      srcY = src.getMinY();
583
584                    // Calculate once for each band, using the smallest
585                    // datatype possible
586                    if (src.getTransferType() == DataBuffer.TYPE_DOUBLE
587                        || src.getTransferType() == DataBuffer.TYPE_FLOAT)
588                      {
589                        pixels = src.getPixel(srcX, srcY, (double[])pixels);
590                        for (int j = 0; j < result.length; j++)
591                          result[j] += ((double[])pixels)[j] * r1 * r2;
592                      }
593                    else
594                      {
595                        pixels = src.getPixel(srcX, srcY, (int[])pixels);
596                        for (int j = 0; j < result.length; j++)
597                          result[j] += ((int[])pixels)[j] * r1 * r2;
598                      }
599                  }
600  
601              // Put it all together
602              dst.setPixel((int)dpts[i] + dst.getMinX(),
603                           (int)dpts[i+1] + dst.getMinY(),
604                           result);
605            }
606        }
607    }
608}