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    
005    This file is part of GNU Classpath.
006    
007    GNU Classpath is free software; you can redistribute it and/or modify
008    it under the terms of the GNU General Public License as published by
009    the Free Software Foundation; either version 2, or (at your option)
010    any later version.
011    
012    GNU Classpath is distributed in the hope that it will be useful, but
013    WITHOUT ANY WARRANTY; without even the implied warranty of
014    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015    General Public License for more details.
016    
017    You should have received a copy of the GNU General Public License
018    along with GNU Classpath; see the file COPYING.  If not, write to the
019    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
020    02110-1301 USA.
021    
022    Linking this library statically or dynamically with other modules is
023    making a combined work based on this library.  Thus, the terms and
024    conditions of the GNU General Public License cover the whole
025    combination.
026    
027    As a special exception, the copyright holders of this library give you
028    permission to link this library with independent modules to produce an
029    executable, regardless of the license terms of these independent
030    modules, and to copy and distribute the resulting executable under
031    terms of your choice, provided that you also meet, for each linked
032    independent module, the terms and conditions of the license of that
033    module.  An independent module is a module which is not derived from
034    or based on this library.  If you modify this library, you may extend
035    this exception to your version of the library, but you are not
036    obligated to do so.  If you do not wish to do so, delete this
037    exception statement from your version. */
038    
039    package java.awt.image;
040    
041    import java.awt.Graphics2D;
042    import java.awt.Point;
043    import java.awt.Rectangle;
044    import java.awt.RenderingHints;
045    import java.awt.geom.AffineTransform;
046    import java.awt.geom.NoninvertibleTransformException;
047    import java.awt.geom.Point2D;
048    import java.awt.geom.Rectangle2D;
049    import 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     */
061    public 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    }