/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @author Oleg V. Khaschansky, Denis M. Kishenko * @version $Revision$ */ package java.awt.image; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.awt.geom.Point2D; import java.awt.geom.NoninvertibleTransformException; import java.awt.*; import java.util.Arrays; import org.apache.harmony.awt.gl.AwtImageBackdoorAccessor; import org.apache.harmony.awt.internal.nls.Messages; /** * The AffineTransform class translates coordinates from 2D coordinates in the * source image or Raster to 2D coordinates in the destination image or Raster * using affine transformation. The number of bands in the source Raster should * equal to the number of bands in the destination Raster. * * @since Android 1.0 */ public class AffineTransformOp implements BufferedImageOp, RasterOp { /** * The Constant TYPE_NEAREST_NEIGHBOR indicates nearest-neighbor * interpolation type. */ public static final int TYPE_NEAREST_NEIGHBOR = 1; /** * The Constant TYPE_BILINEAR indicates bilinear interpolation type. */ public static final int TYPE_BILINEAR = 2; /** * The Constant TYPE_BICUBIC indicates bi-cubic interpolation type. */ public static final int TYPE_BICUBIC = 3; /** * The i type. */ private int iType; // interpolation type /** * The at. */ private AffineTransform at; /** * The hints. */ private RenderingHints hints; static { // TODO - uncomment // System.loadLibrary("imageops"); } /** * Instantiates a new AffineTransformOp with the specified AffineTransform * and RenderingHints object which defines the interpolation type. * * @param xform * the AffineTransform. * @param hints * the RenderingHints object which defines the interpolation * type. */ public AffineTransformOp(AffineTransform xform, RenderingHints hints) { this(xform, TYPE_NEAREST_NEIGHBOR); this.hints = hints; if (hints != null) { Object hint = hints.get(RenderingHints.KEY_INTERPOLATION); if (hint != null) { // Nearest neighbor is default if (hint == RenderingHints.VALUE_INTERPOLATION_BILINEAR) { this.iType = TYPE_BILINEAR; } else if (hint == RenderingHints.VALUE_INTERPOLATION_BICUBIC) { this.iType = TYPE_BICUBIC; } } else { hint = hints.get(RenderingHints.KEY_RENDERING); // Determine from rendering quality if (hint == RenderingHints.VALUE_RENDER_QUALITY) { this.iType = TYPE_BILINEAR; // For speed use nearest neighbor } } } } /** * Instantiates a new AffineTransformOp with the specified AffineTransform * and a specified interpolation type from the list of predefined * interpolation types. * * @param xform * the AffineTransform. * @param interp * the one of predefined interpolation types: * TYPE_NEAREST_NEIGHBOR, TYPE_BILINEAR, or TYPE_BICUBIC. */ public AffineTransformOp(AffineTransform xform, int interp) { if (Math.abs(xform.getDeterminant()) <= Double.MIN_VALUE) { // awt.24F=Unable to invert transform {0} throw new ImagingOpException(Messages.getString("awt.24F", xform)); //$NON-NLS-1$ } this.at = (AffineTransform)xform.clone(); if (interp != TYPE_NEAREST_NEIGHBOR && interp != TYPE_BILINEAR && interp != TYPE_BICUBIC) { // awt.250=Unknown interpolation type: {0} throw new IllegalArgumentException(Messages.getString("awt.250", interp)); //$NON-NLS-1$ } this.iType = interp; } /** * Gets the interpolation type. * * @return the interpolation type. */ public final int getInterpolationType() { return iType; } public final RenderingHints getRenderingHints() { if (hints == null) { Object value = null; switch (iType) { case TYPE_NEAREST_NEIGHBOR: value = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR; break; case TYPE_BILINEAR: value = RenderingHints.VALUE_INTERPOLATION_BILINEAR; break; case TYPE_BICUBIC: value = RenderingHints.VALUE_INTERPOLATION_BICUBIC; break; default: value = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR; } hints = new RenderingHints(RenderingHints.KEY_INTERPOLATION, value); } return hints; } /** * Gets the affine transform associated with this AffineTransformOp. * * @return the AffineTransform. */ public final AffineTransform getTransform() { return (AffineTransform)at.clone(); } public final Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { return at.transform(srcPt, dstPt); } public final Rectangle2D getBounds2D(BufferedImage src) { return getBounds2D(src.getRaster()); } public final Rectangle2D getBounds2D(Raster src) { // We position source raster to (0,0) even if it is translated child // raster. // This means that we need only width and height of the src int width = src.getWidth(); int height = src.getHeight(); float[] corners = { 0, 0, width, 0, width, height, 0, height }; at.transform(corners, 0, corners, 0, 4); Rectangle2D.Float bounds = new Rectangle2D.Float(corners[0], corners[1], 0, 0); bounds.add(corners[2], corners[3]); bounds.add(corners[4], corners[5]); bounds.add(corners[6], corners[7]); return bounds; } public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) { Rectangle2D newBounds = getBounds2D(src); // Destination image should include (0,0) + positive part // of the area bounded by newBounds (in source coordinate system). double dstWidth = newBounds.getX() + newBounds.getWidth(); double dstHeight = newBounds.getY() + newBounds.getHeight(); if (dstWidth <= 0 || dstHeight <= 0) { // awt.251=Transformed width ({0}) and height ({1}) should be // greater than 0 throw new RasterFormatException(Messages.getString("awt.251", dstWidth, dstHeight)); //$NON-NLS-1$ } if (destCM != null) { return new BufferedImage(destCM, destCM.createCompatibleWritableRaster((int)dstWidth, (int)dstHeight), destCM.isAlphaPremultiplied(), null); } ColorModel cm = src.getColorModel(); // Interpolation other than NN doesn't make any sense for index color if (iType != TYPE_NEAREST_NEIGHBOR && cm instanceof IndexColorModel) { return new BufferedImage((int)dstWidth, (int)dstHeight, BufferedImage.TYPE_INT_ARGB); } // OK, we can get source color model return new BufferedImage(cm, src.getRaster().createCompatibleWritableRaster((int)dstWidth, (int)dstHeight), cm.isAlphaPremultiplied(), null); } public WritableRaster createCompatibleDestRaster(Raster src) { // Here approach is other then in createCompatibleDestImage - // destination should include only // transformed image, but not (0,0) in source coordinate system Rectangle2D newBounds = getBounds2D(src); return src.createCompatibleWritableRaster((int)newBounds.getX(), (int)newBounds.getY(), (int)newBounds.getWidth(), (int)newBounds.getHeight()); } public final BufferedImage filter(BufferedImage src, BufferedImage dst) { if (src == dst) { // awt.252=Source can't be same as the destination throw new IllegalArgumentException(Messages.getString("awt.252")); //$NON-NLS-1$ } ColorModel srcCM = src.getColorModel(); BufferedImage finalDst = null; if (srcCM instanceof IndexColorModel && (iType != TYPE_NEAREST_NEIGHBOR || srcCM.getPixelSize() % 8 != 0)) { src = ((IndexColorModel)srcCM).convertToIntDiscrete(src.getRaster(), true); srcCM = src.getColorModel(); } if (dst == null) { dst = createCompatibleDestImage(src, srcCM); } else { if (!srcCM.equals(dst.getColorModel())) { // Treat BufferedImage.TYPE_INT_RGB and // BufferedImage.TYPE_INT_ARGB as same if (!((src.getType() == BufferedImage.TYPE_INT_RGB || src.getType() == BufferedImage.TYPE_INT_ARGB) && (dst .getType() == BufferedImage.TYPE_INT_RGB || dst.getType() == BufferedImage.TYPE_INT_ARGB))) { finalDst = dst; dst = createCompatibleDestImage(src, srcCM); } } } // Skip alpha channel for TYPE_INT_RGB images if (slowFilter(src.getRaster(), dst.getRaster()) != 0) { // awt.21F=Unable to transform source throw new ImagingOpException(Messages.getString("awt.21F")); //$NON-NLS-1$ // TODO - uncomment // if (ippFilter(src.getRaster(), dst.getRaster(), src.getType()) != // 0) // throw new ImagingOpException ("Unable to transform source"); } if (finalDst != null) { Graphics2D g = finalDst.createGraphics(); g.setComposite(AlphaComposite.Src); g.drawImage(dst, 0, 0, null); } else { finalDst = dst; } return finalDst; } public final WritableRaster filter(Raster src, WritableRaster dst) { if (src == dst) { // awt.252=Source can't be same as the destination throw new IllegalArgumentException(Messages.getString("awt.252")); //$NON-NLS-1$ } if (dst == null) { dst = createCompatibleDestRaster(src); } else if (src.getNumBands() != dst.getNumBands()) { // awt.253=Different number of bands in source and destination throw new IllegalArgumentException(Messages.getString("awt.253")); //$NON-NLS-1$ } if (slowFilter(src, dst) != 0) { // awt.21F=Unable to transform source throw new ImagingOpException(Messages.getString("awt.21F")); //$NON-NLS-1$ // TODO - uncomment // if (ippFilter(src, dst, BufferedImage.TYPE_CUSTOM) != 0) // throw new ImagingOpException("Unable to transform source"); } return dst; } // TODO remove when method is used /** * Ipp filter. * * @param src * the src. * @param dst * the dst. * @param imageType * the image type. * @return the int. */ @SuppressWarnings("unused") private int ippFilter(Raster src, WritableRaster dst, int imageType) { int srcStride, dstStride; boolean skipChannel = false; int channels; int offsets[] = null; switch (imageType) { case BufferedImage.TYPE_INT_RGB: case BufferedImage.TYPE_INT_BGR: { channels = 4; srcStride = src.getWidth() * 4; dstStride = dst.getWidth() * 4; skipChannel = true; break; } case BufferedImage.TYPE_INT_ARGB: case BufferedImage.TYPE_INT_ARGB_PRE: case BufferedImage.TYPE_4BYTE_ABGR: case BufferedImage.TYPE_4BYTE_ABGR_PRE: { channels = 4; srcStride = src.getWidth() * 4; dstStride = dst.getWidth() * 4; break; } case BufferedImage.TYPE_BYTE_GRAY: case BufferedImage.TYPE_BYTE_INDEXED: { channels = 1; srcStride = src.getWidth(); dstStride = dst.getWidth(); break; } case BufferedImage.TYPE_3BYTE_BGR: { channels = 3; srcStride = src.getWidth() * 3; dstStride = dst.getWidth() * 3; break; } case BufferedImage.TYPE_USHORT_GRAY: // TODO - could be done in // native code? case BufferedImage.TYPE_USHORT_565_RGB: case BufferedImage.TYPE_USHORT_555_RGB: case BufferedImage.TYPE_BYTE_BINARY: { return slowFilter(src, dst); } default: { SampleModel srcSM = src.getSampleModel(); SampleModel dstSM = dst.getSampleModel(); if (srcSM instanceof PixelInterleavedSampleModel && dstSM instanceof PixelInterleavedSampleModel) { // Check PixelInterleavedSampleModel if (srcSM.getDataType() != DataBuffer.TYPE_BYTE || dstSM.getDataType() != DataBuffer.TYPE_BYTE) { return slowFilter(src, dst); } channels = srcSM.getNumBands(); // Have IPP functions for 1, // 3 and 4 channels if (channels != 1 && channels != 3 && channels != 4) { return slowFilter(src, dst); } int dataTypeSize = DataBuffer.getDataTypeSize(srcSM.getDataType()) / 8; srcStride = ((ComponentSampleModel)srcSM).getScanlineStride() * dataTypeSize; dstStride = ((ComponentSampleModel)dstSM).getScanlineStride() * dataTypeSize; } else if (srcSM instanceof SinglePixelPackedSampleModel && dstSM instanceof SinglePixelPackedSampleModel) { // Check SinglePixelPackedSampleModel SinglePixelPackedSampleModel sppsm1 = (SinglePixelPackedSampleModel)srcSM; SinglePixelPackedSampleModel sppsm2 = (SinglePixelPackedSampleModel)dstSM; // No IPP function for this type if (sppsm1.getDataType() == DataBuffer.TYPE_USHORT) { return slowFilter(src, dst); } channels = sppsm1.getNumBands(); // Have IPP functions for 1, 3 and 4 channels if (channels != 1 && channels != 3 && channels != 4) { return slowFilter(src, dst); } // Check compatibility of sample models if (sppsm1.getDataType() != sppsm2.getDataType() || !Arrays.equals(sppsm1.getBitOffsets(), sppsm2.getBitOffsets()) || !Arrays.equals(sppsm1.getBitMasks(), sppsm2.getBitMasks())) { return slowFilter(src, dst); } for (int i = 0; i < channels; i++) { if (sppsm1.getSampleSize(i) != 8) { return slowFilter(src, dst); } } if (channels == 3) { channels = 4; } int dataTypeSize = DataBuffer.getDataTypeSize(sppsm1.getDataType()) / 8; srcStride = sppsm1.getScanlineStride() * dataTypeSize; dstStride = sppsm2.getScanlineStride() * dataTypeSize; } else { return slowFilter(src, dst); } // Fill offsets if there's a child raster if (src.getParent() != null || dst.getParent() != null) { if (src.getSampleModelTranslateX() != 0 || src.getSampleModelTranslateY() != 0 || dst.getSampleModelTranslateX() != 0 || dst.getSampleModelTranslateY() != 0) { offsets = new int[4]; offsets[0] = -src.getSampleModelTranslateX() + src.getMinX(); offsets[1] = -src.getSampleModelTranslateY() + src.getMinY(); offsets[2] = -dst.getSampleModelTranslateX() + dst.getMinX(); offsets[3] = -dst.getSampleModelTranslateY() + dst.getMinY(); } } } } double m00 = at.getScaleX(); double m01 = at.getShearX(); double m02 = at.getTranslateX(); double m10 = at.getShearY(); double m11 = at.getScaleY(); double m12 = at.getTranslateY(); Object srcData, dstData; AwtImageBackdoorAccessor dbAccess = AwtImageBackdoorAccessor.getInstance(); try { srcData = dbAccess.getData(src.getDataBuffer()); dstData = dbAccess.getData(dst.getDataBuffer()); } catch (IllegalArgumentException e) { return -1; // Unknown data buffer type } return ippAffineTransform(m00, m01, m02, m10, m11, m12, srcData, src.getWidth(), src .getHeight(), srcStride, dstData, dst.getWidth(), dst.getHeight(), dstStride, iType, channels, skipChannel, offsets); } /** * Slow filter. * * @param src * the src. * @param dst * the dst. * @return the int. */ private int slowFilter(Raster src, WritableRaster dst) { // TODO: make correct interpolation // TODO: what if there are different data types? Rectangle srcBounds = src.getBounds(); Rectangle dstBounds = dst.getBounds(); Rectangle normDstBounds = new Rectangle(0, 0, dstBounds.width, dstBounds.height); Rectangle bounds = getBounds2D(src).getBounds().intersection(normDstBounds); AffineTransform inv = null; try { inv = at.createInverse(); } catch (NoninvertibleTransformException e) { return -1; } double[] m = new double[6]; inv.getMatrix(m); int minSrcX = srcBounds.x; int minSrcY = srcBounds.y; int maxSrcX = srcBounds.x + srcBounds.width; int maxSrcY = srcBounds.y + srcBounds.height; int minX = bounds.x + dstBounds.x; int minY = bounds.y + dstBounds.y; int maxX = minX + bounds.width; int maxY = minY + bounds.height; int hx = (int)(m[0] * 256); int hy = (int)(m[1] * 256); int vx = (int)(m[2] * 256); int vy = (int)(m[3] * 256); int sx = (int)(m[4] * 256) + hx * bounds.x + vx * bounds.y + (srcBounds.x) * 256; int sy = (int)(m[5] * 256) + hy * bounds.x + vy * bounds.y + (srcBounds.y) * 256; vx -= hx * bounds.width; vy -= hy * bounds.width; if (src.getTransferType() == dst.getTransferType()) { for (int y = minY; y < maxY; y++) { for (int x = minX; x < maxX; x++) { int px = sx >> 8; int py = sy >> 8; if (px >= minSrcX && py >= minSrcY && px < maxSrcX && py < maxSrcY) { Object val = src.getDataElements(px, py, null); dst.setDataElements(x, y, val); } sx += hx; sy += hy; } sx += vx; sy += vy; } } else { float pixel[] = null; for (int y = minY; y < maxY; y++) { for (int x = minX; x < maxX; x++) { int px = sx >> 8; int py = sy >> 8; if (px >= minSrcX && py >= minSrcY && px < maxSrcX && py < maxSrcY) { pixel = src.getPixel(px, py, pixel); dst.setPixel(x, y, pixel); } sx += hx; sy += hy; } sx += vx; sy += vy; } } return 0; } /** * Ipp affine transform. * * @param m00 * the m00. * @param m01 * the m01. * @param m02 * the m02. * @param m10 * the m10. * @param m11 * the m11. * @param m12 * the m12. * @param src * the src. * @param srcWidth * the src width. * @param srcHeight * the src height. * @param srcStride * the src stride. * @param dst * the dst. * @param dstWidth * the dst width. * @param dstHeight * the dst height. * @param dstStride * the dst stride. * @param iType * the i type. * @param channels * the channels. * @param skipChannel * the skip channel. * @param offsets * the offsets. * @return the int. */ private native int ippAffineTransform(double m00, double m01, double m02, double m10, double m11, double m12, Object src, int srcWidth, int srcHeight, int srcStride, Object dst, int dstWidth, int dstHeight, int dstStride, int iType, int channels, boolean skipChannel, int offsets[]); }