From 2c4c6d5a48fac18f4d53fad4b83e056ee25b9a92 Mon Sep 17 00:00:00 2001 From: Oliver Schmidtmer Date: Wed, 1 Jun 2016 23:59:00 +0200 Subject: [PATCH] Extended AffineTransformOp for a Graphics2D fallback on filter-method --- .../image/AffineTransformOp.java | 147 +++++++++++ .../image/AffineTransformOpTest.java | 243 ++++++++++++++++++ .../contrib/tiff/TIFFUtilities.java | 2 +- 3 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 common/common-image/src/main/java/com/twelvemonkeys/image/AffineTransformOp.java create mode 100644 common/common-image/src/test/java/com/twelvemonkeys/image/AffineTransformOpTest.java diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/AffineTransformOp.java b/common/common-image/src/main/java/com/twelvemonkeys/image/AffineTransformOp.java new file mode 100644 index 00000000..1adcb661 --- /dev/null +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/AffineTransformOp.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2016, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.*; + +/** + * This is a drop-in replacement for {@link java.awt.image.AffineTransformOp}. + *

Currently only a modification on {@link #filter(BufferedImage, BufferedImage)} is done, which does a Graphics2D fallback for the native lib.

+ * + * @author Oliver Schmidtmer + * @author last modified by $Author$ + * @version $Id$ + */ +public class AffineTransformOp implements BufferedImageOp, RasterOp { + + java.awt.image.AffineTransformOp delegate; + + public static final int TYPE_NEAREST_NEIGHBOR = java.awt.image.AffineTransformOp.TYPE_NEAREST_NEIGHBOR; + + public static final int TYPE_BILINEAR = java.awt.image.AffineTransformOp.TYPE_BILINEAR; + + public static final int TYPE_BICUBIC = java.awt.image.AffineTransformOp.TYPE_BICUBIC; + + /** + * @param xform The {@link AffineTransform} to use for the operation. + * @param hints The {@link RenderingHints} object used to specify the interpolation type for the operation. + */ + public AffineTransformOp(AffineTransform xform, + RenderingHints hints) { + delegate = new java.awt.image.AffineTransformOp(xform, hints); + } + + /** + * @param xform The {@link AffineTransform} to use for the operation. + * @param interpolationType One of the integer interpolation type constants defined by this class: {@link #TYPE_NEAREST_NEIGHBOR}, {@link #TYPE_BILINEAR}, {@link #TYPE_BICUBIC}. + */ + public AffineTransformOp(AffineTransform xform, + int interpolationType) { + delegate = new java.awt.image.AffineTransformOp(xform, interpolationType); + } + + @Override + public BufferedImage filter(BufferedImage src, + BufferedImage dst) { + try { + return delegate.filter(src, dst); + } + catch (ImagingOpException ex) { + if (dst == null) { + dst = createCompatibleDestImage(src, src.getColorModel()); + } + Graphics2D g2d = null; + try { + g2d = dst.createGraphics(); + int interpolationType = delegate.getInterpolationType(); + if (interpolationType > 0) { + Object interpolationValue = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR; + switch (interpolationType) { + case java.awt.image.AffineTransformOp.TYPE_BILINEAR: + interpolationValue = RenderingHints.VALUE_INTERPOLATION_BILINEAR; + break; + case java.awt.image.AffineTransformOp.TYPE_BICUBIC: + interpolationValue = RenderingHints.VALUE_INTERPOLATION_BICUBIC; + break; + } + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolationValue); + } + else if (getRenderingHints() != null) { + g2d.setRenderingHints(getRenderingHints()); + } + g2d.drawImage(src, delegate.getTransform(), null); + return dst; + } + finally { + if (g2d != null) { + g2d.dispose(); + } + } + } + } + + @Override + public Rectangle2D getBounds2D(BufferedImage src) { + return delegate.getBounds2D(src); + } + + @Override + public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) { + return delegate.createCompatibleDestImage(src, destCM); + } + + @Override + public WritableRaster filter(Raster src, WritableRaster dest) { + return delegate.filter(src, dest); + } + + @Override + public Rectangle2D getBounds2D(Raster src) { + return delegate.getBounds2D(src); + } + + @Override + public WritableRaster createCompatibleDestRaster(Raster src) { + return delegate.createCompatibleDestRaster(src); + } + + @Override + public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { + return delegate.getPoint2D(srcPt, dstPt); + } + + @Override + public RenderingHints getRenderingHints() { + return delegate.getRenderingHints(); + } +} diff --git a/common/common-image/src/test/java/com/twelvemonkeys/image/AffineTransformOpTest.java b/common/common-image/src/test/java/com/twelvemonkeys/image/AffineTransformOpTest.java new file mode 100644 index 00000000..791af181 --- /dev/null +++ b/common/common-image/src/test/java/com/twelvemonkeys/image/AffineTransformOpTest.java @@ -0,0 +1,243 @@ +package com.twelvemonkeys.image; + +import org.junit.Test; + +import javax.imageio.ImageTypeSpecifier; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.image.*; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * AffineTransformOpTest. + * + * @author Harald Kuhr + * @author Oliver Schmidtmer + * @author last modified by $Author: harald.kuhr$ + * @version $Id: AffineTransformOpTest.java,v 1.0 03/06/16 harald.kuhr Exp$ + */ +public class AffineTransformOpTest { + // Some notes: + // It would be nice to have the following classes from imageio-core available: + // - ColorSpaces (for CMYK testing) + // - ImageTypeSpecifiers (for correct specs) + // Would perhaps be better to use parameterized test case + // Is it enough to test only (quadrant) rotation? Or should we test scale/translate/arbitrary rotation etc? + + // TYPE_INT_RGB == 1 (min), TYPE_BYTE_INDEXED == 13 (max), TYPE_CUSTOM (0) excluded + private static final List TYPES = Arrays.asList( + BufferedImage.TYPE_INT_RGB, + BufferedImage.TYPE_INT_ARGB, + BufferedImage.TYPE_INT_ARGB_PRE, + BufferedImage.TYPE_INT_BGR, + BufferedImage.TYPE_3BYTE_BGR, + BufferedImage.TYPE_4BYTE_ABGR, + BufferedImage.TYPE_4BYTE_ABGR_PRE, + BufferedImage.TYPE_USHORT_565_RGB, + BufferedImage.TYPE_USHORT_555_RGB, + BufferedImage.TYPE_BYTE_GRAY, + BufferedImage.TYPE_USHORT_GRAY, + BufferedImage.TYPE_BYTE_BINARY, + BufferedImage.TYPE_BYTE_BINARY + ); + + private static final ColorSpace GRAY = ColorSpace.getInstance(ColorSpace.CS_GRAY); + private static final ColorSpace S_RGB = ColorSpace.getInstance(ColorSpace.CS_sRGB); + + // Most of these will fail using the standard Op + private static final List SPECS = Arrays.asList( + ImageTypeSpecifier.createInterleaved(GRAY, new int[] {0, 1}, DataBuffer.TYPE_USHORT, true, false), + ImageTypeSpecifier.createInterleaved(GRAY, new int[] {0, 1}, DataBuffer.TYPE_SHORT, true, false), + ImageTypeSpecifier.createInterleaved(GRAY, new int[] {0, 1}, DataBuffer.TYPE_INT, true, false), + ImageTypeSpecifier.createInterleaved(GRAY, new int[] {0, 1}, DataBuffer.TYPE_FLOAT, true, false), + ImageTypeSpecifier.createInterleaved(GRAY, new int[] {0, 1}, DataBuffer.TYPE_DOUBLE, true, false), + + ImageTypeSpecifier.createInterleaved(S_RGB, new int[] {0, 1, 2}, DataBuffer.TYPE_USHORT, false, false), + ImageTypeSpecifier.createInterleaved(S_RGB, new int[] {0, 1, 2}, DataBuffer.TYPE_SHORT, false, false), + ImageTypeSpecifier.createInterleaved(S_RGB, new int[] {0, 1, 2}, DataBuffer.TYPE_INT, false, false), + ImageTypeSpecifier.createInterleaved(S_RGB, new int[] {0, 1, 2}, DataBuffer.TYPE_FLOAT, false, false), + ImageTypeSpecifier.createInterleaved(S_RGB, new int[] {0, 1, 2}, DataBuffer.TYPE_DOUBLE, false, false), + + ImageTypeSpecifier.createInterleaved(S_RGB, new int[] {0, 1, 2, 3}, DataBuffer.TYPE_USHORT, true, false), + ImageTypeSpecifier.createInterleaved(S_RGB, new int[] {0, 1, 2, 3}, DataBuffer.TYPE_SHORT, true, false), + ImageTypeSpecifier.createInterleaved(S_RGB, new int[] {0, 1, 2, 3}, DataBuffer.TYPE_INT, true, false), + ImageTypeSpecifier.createInterleaved(S_RGB, new int[] {0, 1, 2, 3}, DataBuffer.TYPE_FLOAT, true, false), + ImageTypeSpecifier.createInterleaved(S_RGB, new int[] {0, 1, 2, 3}, DataBuffer.TYPE_DOUBLE, true, false) + ); + + final int width = 30; + final int height = 20; + + @Test + public void testGetPoint2D() { + AffineTransform rotateInstance = AffineTransform.getRotateInstance(2.1); + BufferedImageOp original = new java.awt.image.AffineTransformOp(rotateInstance, null); + BufferedImageOp fallback = new com.twelvemonkeys.image.AffineTransformOp(rotateInstance, null); + + Point2D point = new Point2D.Double(39.7, 42.91); + assertEquals(original.getPoint2D(point, null), fallback.getPoint2D(point, null)); + } + + @Test + public void testGetBounds2D() { + AffineTransform shearInstance = AffineTransform.getShearInstance(33.77, 77.33); + BufferedImageOp original = new java.awt.image.AffineTransformOp(shearInstance, null); + BufferedImageOp fallback = new com.twelvemonkeys.image.AffineTransformOp(shearInstance, null); + + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + + assertEquals(original.getBounds2D(image), fallback.getBounds2D(image)); + } + + // TODO: ...etc. For all delegated methods, just test that it does exactly what the original does. + // It won't test much for now, but it will make sure we don't accidentally break things in the future. + + @Test + public void testFilterRotateBIStandard() { + BufferedImageOp op_jre = new java.awt.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, Math.min(width, height) / 2, Math.min(width, height) / 2), null); + BufferedImageOp op_tm = new com.twelvemonkeys.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, Math.min(width, height) / 2, Math.min(width, height) / 2), null); + + for (Integer type : TYPES) { + BufferedImage image = new BufferedImage(width, height, type); + BufferedImage result_jre = op_jre.filter(image, null); + BufferedImage result_tm = op_tm.filter(image, null); + + assertNotNull("No result!", result_tm); + assertEquals("Bad type", result_jre.getType(), result_tm.getType()); + assertEquals("Incorrect color model", result_jre.getColorModel(), result_tm.getColorModel()); + + assertEquals("Incorrect width", result_jre.getWidth(), result_tm.getWidth()); + assertEquals("Incorrect height", result_jre.getHeight(), result_tm.getHeight()); + } + } + + @Test + public void testFilterRotateBICustom() { + BufferedImageOp op_jre = new java.awt.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, Math.min(width, height) / 2, Math.min(width, height) / 2), null); + BufferedImageOp op_tm = new com.twelvemonkeys.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, Math.min(width, height) / 2, Math.min(width, height) / 2), null); + + for (ImageTypeSpecifier spec : SPECS) { + BufferedImage image = spec.createBufferedImage(width, height); + + BufferedImage result_tm = op_tm.filter(image, null); + assertNotNull("No result!", result_tm); + + BufferedImage result_jre; + try { + result_jre = op_jre.filter(image, null); + + assertEquals("Bad type", result_jre.getType(), result_tm.getType()); + assertEquals("Incorrect color model", result_jre.getColorModel(), result_tm.getColorModel()); + + assertEquals("Incorrect width", result_jre.getWidth(), result_tm.getWidth()); + assertEquals("Incorrect height", result_jre.getHeight(), result_tm.getHeight()); + } + catch (ImagingOpException e) { + System.err.println("spec: " + spec); + assertEquals("Bad type", spec.getBufferedImageType(), result_tm.getType()); + assertEquals("Incorrect color model", spec.getColorModel(), result_tm.getColorModel()); + + assertEquals("Incorrect width", height, result_tm.getWidth()); + assertEquals("Incorrect height", width, result_tm.getHeight()); + } + } + } + + // TODO: Test RasterOp variants of filter too + + @Test + public void testRasterGetBounds2D() { + AffineTransform shearInstance = AffineTransform.getShearInstance(33.77, 77.33); + RasterOp original = new java.awt.image.AffineTransformOp(shearInstance, null); + RasterOp fallback = new com.twelvemonkeys.image.AffineTransformOp(shearInstance, null); + + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + + assertEquals(original.getBounds2D(image.getRaster()), fallback.getBounds2D(image.getRaster())); + } + + @Test + public void testRasterRotateBIStandard() { + RasterOp op_jre = new java.awt.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, Math.min(width, height) / 2, Math.min(width, height) / 2), null); + RasterOp op_tm = new com.twelvemonkeys.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, Math.min(width, height) / 2, Math.min(width, height) / 2), null); + + for (Integer type : TYPES) { + Raster raster = new BufferedImage(width, height, type).getRaster(); + Raster result_jre = null, result_tm = null; + + try { + result_jre = op_jre.filter(raster, null); + } + catch (ImagingOpException e) { + System.err.println("type: " + type); + } + + try { + result_tm = op_tm.filter(raster, null); + } + catch (ImagingOpException e) { + // Only fail if JRE AffineOp produces a result and our version not + if (result_jre != null) { + assertNotNull("No result!", result_tm); + } + else { + continue; + } + } + + if (result_jre != null) { + assertEquals("Incorrect width", result_jre.getWidth(), result_tm.getWidth()); + assertEquals("Incorrect height", result_jre.getHeight(), result_tm.getHeight()); + } + else { + assertEquals("Incorrect width", height, result_tm.getWidth()); + assertEquals("Incorrect height", width, result_tm.getHeight()); + } + } + } + + @Test + public void testRasterRotateBICustom() { + RasterOp op_jre = new java.awt.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, Math.min(width, height) / 2, Math.min(width, height) / 2), null); + RasterOp op_tm = new com.twelvemonkeys.image.AffineTransformOp(AffineTransform.getQuadrantRotateInstance(1, Math.min(width, height) / 2, Math.min(width, height) / 2), null); + + for (ImageTypeSpecifier spec : SPECS) { + Raster raster = spec.createBufferedImage(width, height).getRaster(); + Raster result_jre = null, result_tm = null; + + try { + result_jre = op_jre.filter(raster, null); + } + catch (ImagingOpException e) { + System.err.println("spec: " + spec); + } + + try { + result_tm = op_tm.filter(raster, null); + } + catch (ImagingOpException e) { + // Only fail if JRE AffineOp produces a result and our version not + if (result_jre != null) { + assertNotNull("No result!", result_tm); + } + else { + continue; + } + } + + if (result_jre != null) { + assertEquals("Incorrect width", result_jre.getWidth(), result_tm.getWidth()); + assertEquals("Incorrect height", result_jre.getHeight(), result_tm.getHeight()); + } + else { + assertEquals("Incorrect width", height, result_tm.getWidth()); + assertEquals("Incorrect height", width, result_tm.getHeight()); + } + } + } +} diff --git a/contrib/src/main/java/com/twelvemonkeys/contrib/tiff/TIFFUtilities.java b/contrib/src/main/java/com/twelvemonkeys/contrib/tiff/TIFFUtilities.java index d1b57623..8835c038 100644 --- a/contrib/src/main/java/com/twelvemonkeys/contrib/tiff/TIFFUtilities.java +++ b/contrib/src/main/java/com/twelvemonkeys/contrib/tiff/TIFFUtilities.java @@ -28,6 +28,7 @@ package com.twelvemonkeys.contrib.tiff; +import com.twelvemonkeys.image.AffineTransformOp; import com.twelvemonkeys.imageio.metadata.*; import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; import com.twelvemonkeys.imageio.metadata.exif.EXIFWriter; @@ -40,7 +41,6 @@ import javax.imageio.ImageIO; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; import java.awt.geom.AffineTransform; -import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException;