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;