OSDN Git Service

Merged gcj-eclipse branch to trunk.
[pf3gnuchains/gcc-fork.git] / libjava / classpath / java / awt / image / AffineTransformOp.java
index bb4b795..849c5b0 100644 (file)
@@ -1,6 +1,6 @@
 /* AffineTransformOp.java --  This class performs affine 
    transformation between two images or rasters in 2 dimensions.
-   Copyright (C) 2004 Free Software Foundation
+   Copyright (C) 2004, 2006 Free Software Foundation
 
 This file is part of GNU Classpath.
 
@@ -39,6 +39,7 @@ exception statement from your version. */
 package java.awt.image;
 
 import java.awt.Graphics2D;
+import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.RenderingHints;
 import java.awt.geom.AffineTransform;
@@ -48,10 +49,14 @@ import java.awt.geom.Rectangle2D;
 import java.util.Arrays;
 
 /**
- * This class performs affine transformation between two images or 
- * rasters in 2 dimensions. 
+ * AffineTransformOp performs matrix-based transformations (translations,
+ * scales, flips, rotations, and shears).
+ * 
+ * If interpolation is required, nearest neighbour, bilinear, and bicubic
+ * methods are available.
  *
  * @author Olga Rodimina (rodimina@redhat.com) 
+ * @author Francis Kung (fkung@redhat.com)
  */
 public class AffineTransformOp implements BufferedImageOp, RasterOp
 {
@@ -74,6 +79,7 @@ public class AffineTransformOp implements BufferedImageOp, RasterOp
      *
      * @param xform AffineTransform that will applied to the source image 
      * @param interpolationType type of interpolation used
+     * @throws ImagingOpException if the transform matrix is noninvertible
      */
     public AffineTransformOp (AffineTransform xform, int interpolationType)
     {
@@ -102,6 +108,7 @@ public class AffineTransformOp implements BufferedImageOp, RasterOp
      * 
      * @param xform AffineTransform that will applied to the source image
      * @param hints rendering hints that will be used during transformation
+     * @throws ImagingOpException if the transform matrix is noninvertible
      */
     public AffineTransformOp (AffineTransform xform, RenderingHints hints)
     {
@@ -112,185 +119,165 @@ public class AffineTransformOp implements BufferedImageOp, RasterOp
     }
 
     /**
-     * Creates empty BufferedImage with the size equal to that of the 
-     * transformed image and correct number of bands. The newly created 
+     * Creates a new BufferedImage with the size equal to that of the 
+     * transformed image and the correct number of bands. The newly created 
      * image is created with the specified ColorModel. 
-     * If the ColorModel is equal to null, then image is created 
-     * with the ColorModel of the source image.
+     * If a ColorModel is not specified, an appropriate ColorModel is used.
      *
-     * @param src source image
-     * @param destCM color model for the destination image
-     * @return new compatible destination image
+     * @param src the source image.
+     * @param destCM color model for the destination image (can be null).
+     * @return a new compatible destination image.
      */
     public BufferedImage createCompatibleDestImage (BufferedImage src,
                                                     ColorModel destCM)
     {
+      if (destCM != null)
+        return new BufferedImage(destCM,
+                                 createCompatibleDestRaster(src.getRaster()),
+                                 src.isAlphaPremultiplied(), null);
+
+      // This behaviour was determined by Mauve testcases, and is compatible
+      // with the reference implementation
+      if (src.getType() == BufferedImage.TYPE_INT_ARGB_PRE
+          || src.getType() == BufferedImage.TYPE_4BYTE_ABGR
+          || src.getType() == BufferedImage.TYPE_4BYTE_ABGR_PRE)
+        return new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
 
-      // if destCm is not specified, use color model of the source image
-
-      if (destCM == null) 
-        destCM = src.getColorModel ();
-
-      return new BufferedImage (destCM, 
-                                createCompatibleDestRaster (src.getRaster ()),
-                                src.isAlphaPremultiplied (),
-                                null);                      
-
+      else
+        return new BufferedImage(src.getWidth(), src.getHeight(),
+                                 BufferedImage.TYPE_INT_ARGB);
     }
 
     /**
-     * Creates empty WritableRaster with the size equal to the transformed 
-     * source raster and correct number of bands 
+     * Creates a new WritableRaster with the size equal to the transformed 
+     * source raster and correct number of bands .
      *
-     * @param src source raster
-     * @throws RasterFormatException if resulting width or height of raster is 0
-     * @return new compatible raster
+     * @param src the source raster.
+     * @throws RasterFormatException if resulting width or height of raster is 0.
+     * @return a new compatible raster.
      */
     public WritableRaster createCompatibleDestRaster (Raster src)
     {
-      Rectangle rect = (Rectangle) getBounds2D (src);
+      Rectangle2D rect = getBounds2D(src);
       
-      // throw RasterFormatException if resulting width or height of the
-      // transformed raster is 0
-
-      if (rect.getWidth () == 0 || rect.getHeight () == 0) 
+      if (rect.getWidth() == 0 || rect.getHeight() == 0) 
         throw new RasterFormatException("width or height is 0");
 
-      return src.createCompatibleWritableRaster ((int) rect.getWidth (), 
-                                                (int) rect.getHeight ());
+      return src.createCompatibleWritableRaster((int) rect.getWidth(), 
+                                                (int) rect.getHeight());
     }
 
     /**
      * Transforms source image using transform specified at the constructor.
-     * The resulting transformed image is stored in the destination image. 
+     * The resulting transformed image is stored in the destination image if one
+     * is provided; otherwise a new BufferedImage is created and returned. 
      *
      * @param src source image
      * @param dst destination image
-     * @return transformed source image
+     * @throws IllegalArgumentException if the source and destination image are
+     *          the same
+     * @return transformed source image.
      */
     public final BufferedImage filter (BufferedImage src, BufferedImage dst)
     {
-
       if (dst == src)
-        throw new IllegalArgumentException ("src image cannot be the same as the dst image");
-
-      // If the destination image is null, then BufferedImage is 
-      // created with ColorModel of the source image
+        throw new IllegalArgumentException("src image cannot be the same as "
+                                         + "the dst image");
 
+      // If the destination image is null, then use a compatible BufferedImage
       if (dst == null)
-        dst = createCompatibleDestImage(src, src.getColorModel ());
-
-      // FIXME: Must check if color models of src and dst images are the same.
-      // If it is not, then source image should be converted to color model
-      // of the destination image
+        dst = createCompatibleDestImage(src, null);
 
-      Graphics2D gr = (Graphics2D) dst.createGraphics ();
-      gr.setRenderingHints (hints);    
-      gr.drawImage (src, transform, null);
+      Graphics2D gr = (Graphics2D) dst.createGraphics();
+      gr.setRenderingHints(hints);
+      gr.drawImage(src, transform, null);
       return dst;
-
     }
 
     /**
      * Transforms source raster using transform specified at the constructor.
-     * The resulting raster is stored in the destination raster.
+     * The resulting raster is stored in the destination raster if it is not
+     * null, otherwise a new raster is created and returned.
      *
      * @param src source raster
      * @param dst destination raster
-     * @return transformed raster
+     * @throws IllegalArgumentException if the source and destination are not
+     *          compatible
+     * @return transformed raster.
      */
-    public final WritableRaster filter (Raster src, WritableRaster dst)
+    public final WritableRaster filter(Raster src, WritableRaster dst)
     {
+      // Initial checks
       if (dst == src)
         throw new IllegalArgumentException("src image cannot be the same as"
-                                          + " the dst image");
+                                           + " the dst image");
 
       if (dst == null)
         dst = createCompatibleDestRaster(src);
 
       if (src.getNumBands() != dst.getNumBands())
         throw new IllegalArgumentException("src and dst must have same number"
-                                          + " of bands");
+                                           + " of bands");
       
-      double[] dpts = new double[dst.getWidth() * 2];
-      double[] pts = new double[dst.getWidth() * 2];
+      // Optimization for rasters that can be represented in the RGB colormodel:
+      // wrap the rasters in images, and let Cairo do the transformation
+      if (ColorModel.getRGBdefault().isCompatibleSampleModel(src.getSampleModel())
+          && ColorModel.getRGBdefault().isCompatibleSampleModel(dst.getSampleModel()))
+        {
+          WritableRaster src2 = Raster.createWritableRaster(src.getSampleModel(),
+                                                            src.getDataBuffer(),
+                                                            new Point(src.getMinX(),
+                                                                      src.getMinY()));
+          BufferedImage iSrc = new BufferedImage(ColorModel.getRGBdefault(),
+                                                 src2, false, null);
+          BufferedImage iDst = new BufferedImage(ColorModel.getRGBdefault(), dst,
+                                                 false, null);
+  
+          return filter(iSrc, iDst).getRaster();
+        }
+
+      // Otherwise, we need to do the transformation in java code...
+      // Create arrays to hold all the points
+      double[] dstPts = new double[dst.getHeight() * dst.getWidth() * 2];
+      double[] srcPts = new double[dst.getHeight() * dst.getWidth() * 2];
+
+      // Populate array with all points in the *destination* raster
+      int i = 0;
       for (int x = 0; x < dst.getWidth(); x++)
-      {
-       dpts[2 * x] = x + dst.getMinX();
-       dpts[2 * x + 1] = x;
-      }
-      Rectangle srcbounds = src.getBounds();
-      if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR))
-      {
-       for (int y = dst.getMinY(); y < dst.getMinY() + dst.getHeight(); y++)
-         {
-           try {
-             transform.inverseTransform(dpts, 0, pts, 0, dst.getWidth() * 2);
-           } catch (NoninvertibleTransformException e) {
-             // Can't happen since the constructor traps this
-             e.printStackTrace();
-           }
-        
-           for (int x = 0; x < dst.getWidth(); x++)
-             {
-               if (!srcbounds.contains(pts[2 * x], pts[2 * x + 1]))
-                 continue;
-               dst.setDataElements(x + dst.getMinX(), y,
-                                   src.getDataElements((int)pts[2 * x],
-                                                       (int)pts[2 * x + 1],
-                                                       null));
-             }
-         }
-      }
-      else if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_BILINEAR))
-      {
-        double[] tmp = new double[4 * src.getNumBands()];
-        for (int y = dst.getMinY(); y < dst.getMinY() + dst.getHeight(); y++)
         {
-          try {
-            transform.inverseTransform(dpts, 0, pts, 0, dst.getWidth() * 2);
-          } catch (NoninvertibleTransformException e) {
-            // Can't happen since the constructor traps this
-            e.printStackTrace();
-          }
-           
-          for (int x = 0; x < dst.getWidth(); x++)
-          {
-            if (!srcbounds.contains(pts[2 * x], pts[2 * x + 1]))
-              continue;
-            int xx = (int)pts[2 * x];
-            int yy = (int)pts[2 * x + 1];
-            double dx = (pts[2 * x] - xx);
-            double dy = (pts[2 * x + 1] - yy);
-               
-            // TODO write this more intelligently
-            if (xx == src.getMinX() + src.getWidth() - 1 ||
-                yy == src.getMinY() + src.getHeight() - 1)
+          for (int y = 0; y < dst.getHeight(); y++)
             {
-              // bottom or right edge
-              Arrays.fill(tmp, 0);
-              src.getPixel(xx, yy, tmp);
+              dstPts[i++] = x;
+              dstPts[i++] = y;
             }
-            else
-           {
-              // Normal case
-              src.getPixels(xx, yy, 2, 2, tmp);
-             for (int b = 0; b < src.getNumBands(); b++)
-               tmp[b] = dx * dy * tmp[b]
-                 + (1 - dx) * dy * tmp[b + src.getNumBands()]
-                 + dx * (1 - dy) * tmp[b + 2 * src.getNumBands()]
-                 + (1 - dx) * (1 - dy) * tmp[b + 3 * src.getNumBands()];
-           }
-            dst.setPixel(x, y, tmp);
-          }
         }
-      }
-      else
-      {
-        // Bicubic
-        throw new UnsupportedOperationException("not implemented yet");
-      }
+      Rectangle srcbounds = src.getBounds();
+
+      // Use an inverse transform to map each point in the destination to
+      // a point in the source.  Note that, while all points in the destination
+      // matrix are integers, this is not necessarily true for points in the
+      // source (hence why interpolation is required) 
+      try
+        {
+          AffineTransform inverseTx = transform.createInverse();
+          inverseTx.transform(dstPts, 0, srcPts, 0, dstPts.length / 2);
+        }
+      catch (NoninvertibleTransformException e)
+        {
+          // Shouldn't happen since the constructor traps this
+          throw new ImagingOpException(e.getMessage());
+        }
+
+      // Different interpolation methods...
+      if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR))
+        filterNearest(src, dst, dstPts, srcPts);
       
+      else if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_BILINEAR))
+        filterBilinear(src, dst, dstPts, srcPts);
+    
+      else          // bicubic
+        filterBicubic(src, dst, dstPts, srcPts);
+
       return dst;  
     }
 
@@ -314,27 +301,22 @@ public class AffineTransformOp implements BufferedImageOp, RasterOp
      */
     public final Rectangle2D getBounds2D (Raster src)
     {
-      // determine new size for the transformed raster.
-      // Need to calculate transformed coordinates of the lower right
-      // corner of the raster. The upper left corner is always (0,0)
-              
-      double x2 = (double) src.getWidth () + src.getMinX ();
-      double y2 = (double) src.getHeight () + src.getMinY ();
-      Point2D p2 = getPoint2D (new Point2D.Double (x2,y2), null);
-
-      Rectangle2D rect = new Rectangle (0, 0, (int) p2.getX (), (int) p2.getY ());
-      return rect.getBounds ();
+      return transform.createTransformedShape(src.getBounds()).getBounds2D();
     }
 
     /**
-     * Returns interpolation type used during transformations
+     * Returns interpolation type used during transformations.
      *
      * @return interpolation type
      */
     public final int getInterpolationType ()
     {
-      if(hints.containsValue (RenderingHints.VALUE_INTERPOLATION_BILINEAR))
+      if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_BILINEAR))
         return TYPE_BILINEAR;
+      
+      else if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_BICUBIC))
+        return TYPE_BICUBIC;
+      
       else 
         return TYPE_NEAREST_NEIGHBOR;
     }
@@ -355,7 +337,7 @@ public class AffineTransformOp implements BufferedImageOp, RasterOp
     /**
      * Returns rendering hints that are used during transformation.
      *
-     * @return rendering hints
+     * @return the rendering hints used in this Op.
      */
     public final RenderingHints getRenderingHints ()
     {
@@ -366,10 +348,261 @@ public class AffineTransformOp implements BufferedImageOp, RasterOp
      * Returns transform used in transformation between source and destination
      * image.
      *
-     * @return transform
+     * @return the transform used in this Op.
      */
     public final AffineTransform getTransform ()
     {
       return transform;
     }
+    
+    /**
+     * Perform nearest-neighbour filtering
+     * 
+     * @param src the source raster
+     * @param dst the destination raster
+     * @param dpts array of points on the destination raster
+     * @param pts array of corresponding points on the source raster
+     */
+    private void filterNearest(Raster src, WritableRaster dst, double[] dpts,
+                               double[] pts)
+    {
+      Rectangle srcbounds = src.getBounds();
+  
+      // For all points on the destination raster, copy the value from the
+      // corrosponding (rounded) source point
+      for (int i = 0; i < dpts.length; i += 2)
+        {
+          int srcX = (int) Math.round(pts[i]) + src.getMinX();
+          int srcY = (int) Math.round(pts[i + 1]) + src.getMinY();
+          
+          if (srcbounds.contains(srcX, srcY))
+            dst.setDataElements((int) dpts[i] + dst.getMinX(),
+                                (int) dpts[i + 1] + dst.getMinY(),
+                                src.getDataElements(srcX, srcY, null));
+        }
+    }
+
+    /**
+     * Perform bilinear filtering
+     * 
+     * @param src the source raster
+     * @param dst the destination raster
+     * @param dpts array of points on the destination raster
+     * @param pts array of corresponding points on the source raster
+     */
+    private void filterBilinear(Raster src, WritableRaster dst, double[] dpts,
+                              double[] pts)
+    {
+      Rectangle srcbounds = src.getBounds();
+  
+      Object xyarr = null;
+      Object xp1arr = null;
+      Object yp1arr = null;
+      Object xyp1arr = null;
+      
+      double xy;
+      double xp1;
+      double yp1;
+      double xyp1;
+
+      double[] result = new double[src.getNumBands()];
+      
+      // For all points in the destination raster, use bilinear interpolation
+      // to find the value from the corrosponding source points
+      for (int i = 0; i < dpts.length; i += 2)
+        {
+          int srcX = (int) Math.round(pts[i]) + src.getMinX();
+          int srcY = (int) Math.round(pts[i + 1]) + src.getMinY();
+          
+          if (srcbounds.contains(srcX, srcY))
+            {
+              // Corner case at the bottom or right edge; use nearest neighbour
+              if (pts[i] >= src.getWidth() - 1
+                  || pts[i + 1] >= src.getHeight() - 1)
+                dst.setDataElements((int) dpts[i] + dst.getMinX(),
+                                    (int) dpts[i + 1] + dst.getMinY(),
+                                    src.getDataElements(srcX, srcY, null));
+  
+              // Standard case, apply the bilinear formula
+              else
+                {
+                  int x = (int) Math.floor(pts[i] + src.getMinX());
+                  int y = (int) Math.floor(pts[i + 1] + src.getMinY());
+                  double xdiff = pts[i] + src.getMinX() - x;
+                  double ydiff = pts[i + 1] + src.getMinY() - y;
+
+                  // Get surrounding pixels used in interpolation... optimized
+                  // to use the smallest datatype possible.
+                  if (src.getTransferType() == DataBuffer.TYPE_DOUBLE
+                      || src.getTransferType() == DataBuffer.TYPE_FLOAT)
+                    {
+                      xyarr = src.getPixel(x, y, (double[])xyarr);
+                      xp1arr  = src.getPixel(x+1, y, (double[])xp1arr);
+                      yp1arr = src.getPixel(x, y+1, (double[])yp1arr);
+                      xyp1arr = src.getPixel(x+1, y+1, (double[])xyp1arr);
+                    }
+                  else
+                    {
+                      xyarr = src.getPixel(x, y, (int[])xyarr);
+                      xp1arr  = src.getPixel(x+1, y, (int[])xp1arr);
+                      yp1arr = src.getPixel(x, y+1, (int[])yp1arr);
+                      xyp1arr = src.getPixel(x+1, y+1, (int[])xyp1arr);
+                    }
+                  // using 
+                  // array[] pixels = src.getPixels(x, y, 2, 2, pixels);
+                  // instead of doing four individual src.getPixel() calls
+                  // should be faster, but benchmarking shows that it's not...
+                  
+                  // Run interpolation for each band
+                  for (int j = 0; j < src.getNumBands(); j++)
+                    {
+                      // Pull individual sample values out of array
+                      if (src.getTransferType() == DataBuffer.TYPE_DOUBLE
+                          || src.getTransferType() == DataBuffer.TYPE_FLOAT)
+                        {
+                          xy = ((double[])xyarr)[j];
+                          xp1  = ((double[])xp1arr)[j];
+                          yp1 = ((double[])yp1arr)[j];
+                          xyp1 = ((double[])xyp1arr)[j];
+                        }
+                      else
+                        {
+                          xy = ((int[])xyarr)[j];
+                          xp1  = ((int[])xp1arr)[j];
+                          yp1 = ((int[])yp1arr)[j];
+                          xyp1 = ((int[])xyp1arr)[j];
+                        }
+                      
+                      // If all four samples are identical, there's no need to 
+                      // calculate anything
+                      if (xy == xp1 && xy == yp1 && xy == xyp1)
+                        result[j] = xy;
+                      
+                      // Run bilinear interpolation formula
+                      else
+                        result[j] = (xy * (1-xdiff) + xp1 * xdiff) 
+                                      * (1-ydiff) 
+                                    + (yp1 * (1-xdiff) + xyp1 * xdiff)
+                                      * ydiff;
+                    }
+
+                  dst.setPixel((int)dpts[i] + dst.getMinX(),
+                               (int)dpts[i+1] + dst.getMinY(),
+                               result);
+                }
+            }
+        }
+    }
+
+    /**
+     * Perform bicubic filtering
+     * based on http://local.wasp.uwa.edu.au/~pbourke/colour/bicubic/
+     * 
+     * @param src the source raster
+     * @param dst the destination raster
+     * @param dpts array of points on the destination raster
+     * @param pts array of corresponding points on the source raster
+     */
+    private void filterBicubic(Raster src, WritableRaster dst, double[] dpts,
+                               double[] pts)
+    {
+      Rectangle srcbounds = src.getBounds();
+      double[] result = new double[src.getNumBands()];
+      Object pixels = null;
+
+      // For all points on the destination raster, perform bicubic interpolation
+      // from corrosponding source points
+      for (int i = 0; i < dpts.length; i += 2)
+        {
+          if (srcbounds.contains((int) Math.round(pts[i]) + src.getMinX(),
+                                 (int) Math.round(pts[i + 1]) + src.getMinY()))
+            {
+              int x = (int) Math.floor(pts[i] + src.getMinX());
+              int y = (int) Math.floor(pts[i + 1] + src.getMinY());
+              double dx = pts[i] + src.getMinX() - x;
+              double dy = pts[i + 1] + src.getMinY() - y;
+              Arrays.fill(result, 0);
+  
+              for (int m = - 1; m < 3; m++)
+                for (int n = - 1; n < 3; n++)
+                  {
+                    // R(x) = ( P(x+2)^3 - 4 P(x+1)^3 + 6 P(x)^3 - 4 P(x-1)^3 ) / 6
+                    double r1 = 0;
+                    double r2 = 0;
+
+                    // Calculate R(m - dx)
+                    double rx = m - dx + 2;
+                    r1 += rx * rx * rx;
+
+                    rx = m - dx + 1;
+                    if (rx > 0)
+                      r1 -= 4 * rx * rx * rx;
+
+                    rx = m - dx;
+                    if (rx > 0)
+                      r1 += 6 * rx * rx * rx;
+
+                    rx = m - dx - 1;
+                    if (rx > 0)
+                      r1 -= 4 * rx * rx * rx;
+
+                    r1 /= 6;
+
+                    // Calculate R(dy - n);
+                    rx = dy - n + 2;
+                    if (rx > 0)
+                      r2 += rx * rx * rx;
+
+                    rx = dy - n + 1;
+                    if (rx > 0)
+                      r2 -= 4 * rx * rx * rx;
+
+                    rx = dy - n;
+                    if (rx > 0)
+                      r2 += 6 * rx * rx * rx;
+
+                    rx = dy - n - 1;
+                    if (rx > 0)
+                      r2 -= 4 * rx * rx * rx;
+
+                    r2 /= 6;
+
+                    // Calculate F(i+m, j+n) R(m - dx) R(dy - n)
+                    // Check corner cases
+                    int srcX = x + m;
+                    if (srcX >= src.getMinX() + src.getWidth())
+                      srcX = src.getMinX() + src.getWidth() - 1;
+                    else if (srcX < src.getMinX())
+                      srcX = src.getMinX();
+
+                    int srcY = y + n;
+                    if (srcY >= src.getMinY() + src.getHeight())
+                      srcY = src.getMinY() + src.getHeight() - 1;
+                    else if (srcY < src.getMinY())
+                      srcY = src.getMinY();
+
+                    // Calculate once for each band, using the smallest
+                    // datatype possible
+                    if (src.getTransferType() == DataBuffer.TYPE_DOUBLE
+                        || src.getTransferType() == DataBuffer.TYPE_FLOAT)
+                      {
+                        pixels = src.getPixel(srcX, srcY, (double[])pixels);
+                        for (int j = 0; j < result.length; j++)
+                          result[j] += ((double[])pixels)[j] * r1 * r2;
+                      }
+                    else
+                      {
+                        pixels = src.getPixel(srcX, srcY, (int[])pixels);
+                        for (int j = 0; j < result.length; j++)
+                          result[j] += ((int[])pixels)[j] * r1 * r2;
+                      }
+                  }
+  
+              // Put it all together
+              dst.setPixel((int)dpts[i] + dst.getMinX(),
+                           (int)dpts[i+1] + dst.getMinY(),
+                           result);
+            }
+        }
+    }
 }