diff options
Diffstat (limited to 'libjava/classpath/java/awt/image/AffineTransformOp.java')
-rw-r--r-- | libjava/classpath/java/awt/image/AffineTransformOp.java | 513 |
1 files changed, 373 insertions, 140 deletions
diff --git a/libjava/classpath/java/awt/image/AffineTransformOp.java b/libjava/classpath/java/awt/image/AffineTransformOp.java index bb4b795231b..849c5b05048 100644 --- a/libjava/classpath/java/awt/image/AffineTransformOp.java +++ b/libjava/classpath/java/awt/image/AffineTransformOp.java @@ -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); + } + } + } } |