(1);
+ ImageOutputStream outputStream = null;
+ try {
+ File outputFile = new File(outputDirectory, String.format("%04d", pageNo) + ".tif");
+ outputStream = ImageIO.createImageOutputStream(outputFile);
+ outputPages.clear();
+ outputPages.add(tiffPage);
+ writePages(outputStream, outputPages);
+ outputFiles.add(outputFile);
+ }
+ finally {
+ if (outputStream != null) {
+ outputStream.flush();
+ outputStream.close();
+ }
+ }
+ ++pageNo;
+ }
+ }
+ finally {
+ if (input != null) {
+ input.close();
+ }
+ }
+ return outputFiles;
+ }
+
+ /**
+ * Rotates all pages of a TIFF file by changing TIFF.TAG_ORIENTATION.
+ *
+ * NOTICE: TIFF.TAG_ORIENTATION is an advice how the image is meant do be
+ * displayed. Other metadata, such as width and height, relate to the image
+ * as how it is stored. The ImageIO TIFF plugin does not handle orientation.
+ * Use {@link TIFFUtilities#applyOrientation(BufferedImage, int)} for
+ * applying TIFF.TAG_ORIENTATION.
+ *
+ *
+ * @param imageInput
+ * @param imageOutput
+ * @param degree Rotation amount, supports 90�, 180� and 270�.
+ * @throws IOException
+ */
+ public static void rotatePages(ImageInputStream imageInput, ImageOutputStream imageOutput, int degree)
+ throws IOException {
+ rotatePage(imageInput, imageOutput, degree, -1);
+ }
+
+ /**
+ * Rotates a page of a TIFF file by changing TIFF.TAG_ORIENTATION.
+ *
+ * NOTICE: TIFF.TAG_ORIENTATION is an advice how the image is meant do be
+ * displayed. Other metadata, such as width and height, relate to the image
+ * as how it is stored. The ImageIO TIFF plugin does not handle orientation.
+ * Use {@link TIFFUtilities#applyOrientation(BufferedImage, int)} for
+ * applying TIFF.TAG_ORIENTATION.
+ *
+ *
+ * @param imageInput
+ * @param imageOutput
+ * @param degree Rotation amount, supports 90�, 180� and 270�.
+ * @param pageIndex page which should be rotated or -1 for all pages.
+ * @throws IOException
+ */
+ public static void rotatePage(ImageInputStream imageInput, ImageOutputStream imageOutput, int degree, int pageIndex)
+ throws IOException {
+ ImageInputStream input = null;
+ try {
+ List pages = getPages(imageInput);
+ if (pageIndex != -1) {
+ pages.get(pageIndex).rotate(degree);
+ }
+ else {
+ for (TIFFPage tiffPage : pages) {
+ tiffPage.rotate(degree);
+ }
+ }
+ writePages(imageOutput, pages);
+ }
+ finally {
+ if (input != null) {
+ input.close();
+ }
+ }
+ }
+
+ public static List getPages(ImageInputStream imageInput) throws IOException {
+ ArrayList pages = new ArrayList();
+
+ CompoundDirectory IFDs = (CompoundDirectory) new EXIFReader().read(imageInput);
+
+ int pageCount = IFDs.directoryCount();
+ for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
+ pages.add(new TIFFPage(IFDs.getDirectory(pageIndex), imageInput));
+ }
+
+ return pages;
+ }
+
+ public static void writePages(ImageOutputStream imageOutput, List pages) throws IOException {
+ EXIFWriter exif = new EXIFWriter();
+ long nextPagePos = imageOutput.getStreamPosition();
+ if (nextPagePos == 0) {
+ exif.writeTIFFHeader(imageOutput);
+ nextPagePos = imageOutput.getStreamPosition();
+ imageOutput.writeInt(0);
+ }
+ else {
+ // already has pages, so remember place of EOF to replace with
+ // IFD offset
+ nextPagePos -= 4;
+ }
+
+ for (TIFFPage tiffPage : pages) {
+ long ifdOffset = tiffPage.write(imageOutput, exif);
+
+ long tmp = imageOutput.getStreamPosition();
+ imageOutput.seek(nextPagePos);
+ imageOutput.writeInt((int) ifdOffset);
+ imageOutput.seek(tmp);
+ nextPagePos = tmp;
+ imageOutput.writeInt(0);
+ }
+ }
+
+ public static BufferedImage applyOrientation(BufferedImage input, int orientation) {
+ //TODO: Translations are currently off for non quadratic images
+
+ boolean flipExtends = false;
+ AffineTransform transform = AffineTransform.getTranslateInstance(input.getWidth() / 2, input.getHeight() / 2);
+ switch (orientation) {
+ case TIFFBaseline.ORIENTATION_TOPLEFT:
+ // normal
+ return input;
+ case TIFFExtension.ORIENTATION_TOPRIGHT:
+ // flipped vertically
+ transform.scale(-1, 1);
+ break;
+ case TIFFExtension.ORIENTATION_BOTRIGHT:
+ // rotated 180�
+ transform.quadrantRotate(2);
+ break;
+ case TIFFExtension.ORIENTATION_BOTLEFT:
+ // flipped horizontally
+ transform.scale(1, -1);
+ break;
+ case TIFFExtension.ORIENTATION_LEFTTOP:
+ transform.scale(1, -1);
+ transform.quadrantRotate(1);
+ flipExtends = true;
+ break;
+ case TIFFExtension.ORIENTATION_RIGHTTOP:
+ // rotated 90�
+ transform.quadrantRotate(1);
+ flipExtends = true;
+ break;
+ case TIFFExtension.ORIENTATION_RIGHTBOT:
+ transform.scale(-1, 1);
+ transform.quadrantRotate(1);
+ flipExtends = true;
+ break;
+ case TIFFExtension.ORIENTATION_LEFTBOT:
+ // rotated 270�
+ transform.quadrantRotate(3);
+ flipExtends = true;
+ break;
+ }
+
+ int w = input.getWidth();
+ int h = input.getHeight();
+ if (flipExtends) {
+ w = input.getHeight();
+ h = input.getWidth();
+ }
+
+ BufferedImage output = new BufferedImage(w, h, input.getType());
+ transform.translate(-h / 2, -w / 2);
+ ((Graphics2D) output.getGraphics()).drawImage(input, transform, null);
+
+ return output;
+ }
+
+ public static class TIFFPage {
+ private Directory IFD;
+ private ImageInputStream stream;
+
+ private TIFFPage(Directory IFD, ImageInputStream stream) {
+ this.IFD = IFD;
+ this.stream = stream;
+ }
+
+ private long write(ImageOutputStream outputStream, EXIFWriter exifWriter) throws IOException {
+ Entry stipOffsetsEntry = IFD.getEntryById(TIFF.TAG_STRIP_OFFSETS);
+ long[] offsets;
+ if (stipOffsetsEntry.valueCount() == 1) {
+ offsets = new long[] {(long) stipOffsetsEntry.getValue()};
+ }
+ else {
+ offsets = (long[]) stipOffsetsEntry.getValue();
+ }
+
+ Entry stipByteCountsEntry = IFD.getEntryById(TIFF.TAG_STRIP_BYTE_COUNTS);
+ long[] byteCounts;
+ if (stipOffsetsEntry.valueCount() == 1) {
+ byteCounts = new long[] {(long) stipByteCountsEntry.getValue()};
+ }
+ else {
+ byteCounts = (long[]) stipByteCountsEntry.getValue();
+ }
+
+ int[] newOffsets = new int[offsets.length];
+ for (int i = 0; i < offsets.length; i++) {
+ newOffsets[i] = (int) outputStream.getStreamPosition();
+ stream.seek(offsets[i]);
+
+ byte[] buffer = new byte[(int) byteCounts[i]];
+ stream.readFully(buffer);
+ outputStream.write(buffer);
+ }
+
+ ArrayList newIFD = new ArrayList();
+ Iterator it = IFD.iterator();
+ while (it.hasNext()) {
+ newIFD.add(it.next());
+ }
+
+ newIFD.remove(stipOffsetsEntry);
+ newIFD.add(new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, newOffsets));
+ return exifWriter.writeIFD(newIFD, outputStream);
+ }
+
+ /**
+ * Rotates the image by changing TIFF.TAG_ORIENTATION.
+ *
+ * NOTICE: TIFF.TAG_ORIENTATION is an advice how the image is meant do
+ * be displayed. Other metadata, such as width and height, relate to the
+ * image as how it is stored. The ImageIO TIFF plugin does not handle
+ * orientation. Use
+ * {@link TIFFUtilities#applyOrientation(BufferedImage, int)} for
+ * applying TIFF.TAG_ORIENTATION.
+ *
+ *
+ * @param degree Rotation amount, supports 90�, 180� and 270�.
+ */
+ public void rotate(int degree) {
+ Validate.isTrue(degree % 90 == 0 && degree > 0 && degree < 360,
+ "Only rotations by 90, 180 and 270 degree are supported");
+
+ ArrayList newIDFData = new ArrayList<>();
+ Iterator it = IFD.iterator();
+ while (it.hasNext()) {
+ newIDFData.add(it.next());
+ }
+
+ short orientation = TIFFBaseline.ORIENTATION_TOPLEFT;
+ Entry orientationEntry = IFD.getEntryById(TIFF.TAG_ORIENTATION);
+ if (orientationEntry != null) {
+ orientation = ((Number) orientationEntry.getValue()).shortValue();
+ newIDFData.remove(orientationEntry);
+ }
+
+ int steps = degree / 90;
+ for (int i = 0; i < steps; i++) {
+ switch (orientation) {
+ case TIFFBaseline.ORIENTATION_TOPLEFT:
+ orientation = TIFFExtension.ORIENTATION_RIGHTTOP;
+ break;
+ case TIFFExtension.ORIENTATION_TOPRIGHT:
+ orientation = TIFFExtension.ORIENTATION_RIGHTBOT;
+ break;
+ case TIFFExtension.ORIENTATION_BOTRIGHT:
+ orientation = TIFFExtension.ORIENTATION_LEFTBOT;
+ break;
+ case TIFFExtension.ORIENTATION_BOTLEFT:
+ orientation = TIFFExtension.ORIENTATION_LEFTTOP;
+ break;
+ case TIFFExtension.ORIENTATION_LEFTTOP:
+ orientation = TIFFExtension.ORIENTATION_TOPRIGHT;
+ break;
+ case TIFFExtension.ORIENTATION_RIGHTTOP:
+ orientation = TIFFExtension.ORIENTATION_BOTRIGHT;
+ break;
+ case TIFFExtension.ORIENTATION_RIGHTBOT:
+ orientation = TIFFExtension.ORIENTATION_BOTLEFT;
+ break;
+ case TIFFExtension.ORIENTATION_LEFTBOT:
+ orientation = TIFFBaseline.ORIENTATION_TOPLEFT;
+ break;
+ }
+ }
+ newIDFData.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_ORIENTATION, (short) orientation));
+ IFD = new AbstractDirectory(newIDFData) {
+ };
+ }
+ }
+}
diff --git a/contrib/src/test/java/com/twelvemonkeys/contrib/tiff/TIFFUtilitiesTest.java b/contrib/src/test/java/com/twelvemonkeys/contrib/tiff/TIFFUtilitiesTest.java
new file mode 100644
index 00000000..1471000f
--- /dev/null
+++ b/contrib/src/test/java/com/twelvemonkeys/contrib/tiff/TIFFUtilitiesTest.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (c) 2013, 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.contrib.tiff;
+
+import com.twelvemonkeys.imageio.plugins.tiff.TIFFExtension;
+import com.twelvemonkeys.imageio.plugins.tiff.TIFFMedataFormat;
+import com.twelvemonkeys.io.FileUtil;
+import org.junit.Assert;
+import org.junit.Test;
+import org.w3c.dom.Node;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
+import javax.xml.xpath.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferByte;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * TIFFUtilitiesTest
+ *
+ * @author Oliver Schmidtmer
+ * @author last modified by $Author$
+ * @version $Id$
+ */
+public class TIFFUtilitiesTest {
+
+ @Test
+ public void testMerge() throws IOException {
+ // Files from ImageIO TIFF Plugin
+ InputStream stream1 = getClassLoaderResource("/tiff/ccitt/group3_1d.tif").openStream();
+ InputStream stream2 = getClassLoaderResource("/tiff/ccitt/group3_2d.tif").openStream();
+ InputStream stream3 = getClassLoaderResource("/tiff/ccitt/group4.tif").openStream();
+
+ File file1 = File.createTempFile("imageiotest", ".tif");
+ File file2 = File.createTempFile("imageiotest", ".tif");
+ File file3 = File.createTempFile("imageiotest", ".tif");
+ File output = File.createTempFile("imageiotest", ".tif");
+
+ byte[] data;
+
+ data = FileUtil.read(stream1);
+ FileUtil.write(file1, data);
+ stream1.close();
+
+ data = FileUtil.read(stream2);
+ FileUtil.write(file2, data);
+ stream2.close();
+
+ data = FileUtil.read(stream3);
+ FileUtil.write(file3, data);
+ stream3.close();
+
+ List input = Arrays.asList(file1, file2, file3);
+ TIFFUtilities.merge(input, output);
+
+ ImageInputStream iis = ImageIO.createImageInputStream(output);
+ ImageReader reader = ImageIO.getImageReaders(iis).next();
+ reader.setInput(iis);
+ Assert.assertEquals(3, reader.getNumImages(true));
+
+ iis.close();
+ output.delete();
+ file1.delete();
+ file2.delete();
+ file3.delete();
+ }
+
+ @Test
+ public void testSplit() throws IOException {
+ InputStream inputStream = getClassLoaderResource("/contrib/tiff/multipage.tif").openStream();
+ File inputFile = File.createTempFile("imageiotest", "tif");
+ byte[] data = FileUtil.read(inputStream);
+ FileUtil.write(inputFile, data);
+ inputStream.close();
+
+ File outputDirectory = Files.createTempDirectory("imageio").toFile();
+
+ TIFFUtilities.split(inputFile, outputDirectory);
+
+ ImageReader reader = ImageIO.getImageReadersByFormatName("TIF").next();
+
+ File[] outputFiles = outputDirectory.listFiles();
+ Assert.assertEquals(3, outputFiles.length);
+ for (File outputFile : outputFiles) {
+ ImageInputStream iis = ImageIO.createImageInputStream(outputFile);
+ reader.setInput(iis);
+ Assert.assertEquals(1, reader.getNumImages(true));
+ iis.close();
+ outputFile.delete();
+ }
+ outputDirectory.delete();
+ inputFile.delete();
+ }
+
+ @Test
+ public void testRotate() throws IOException, XPathExpressionException {
+ ImageReader reader = ImageIO.getImageReadersByFormatName("TIF").next();
+
+ InputStream inputStream = getClassLoaderResource("/contrib/tiff/multipage.tif").openStream();
+ File inputFile = File.createTempFile("imageiotest", ".tif");
+ byte[] data = FileUtil.read(inputStream);
+ FileUtil.write(inputFile, data);
+ inputStream.close();
+
+ XPath xPath = XPathFactory.newInstance().newXPath();
+ XPathExpression expression = xPath.compile("TIFFIFD/TIFFField[@number='274']/TIFFBytes/TIFFByte/@value");
+
+ // rotate all pages
+ ImageInputStream inputTest1 = ImageIO.createImageInputStream(inputFile);
+ File outputTest1 = File.createTempFile("imageiotest", ".tif");
+ ImageOutputStream iosTest1 = ImageIO.createImageOutputStream(outputTest1);
+ TIFFUtilities.rotatePages(inputTest1, iosTest1, 90);
+ iosTest1.close();
+
+ ImageInputStream checkTest1 = ImageIO.createImageInputStream(outputTest1);
+ reader.setInput(checkTest1);
+ for (int i = 0; i < 3; i++) {
+ Node metaData = reader.getImageMetadata(i)
+ .getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME);
+ short orientation = ((Number) expression.evaluate(metaData, XPathConstants.NUMBER)).shortValue();
+ Assert.assertEquals(orientation, TIFFExtension.ORIENTATION_RIGHTTOP);
+ }
+ checkTest1.close();
+
+ // rotate single page further
+ ImageInputStream inputTest2 = ImageIO.createImageInputStream(outputTest1);
+ File outputTest2 = File.createTempFile("imageiotest", ".tif");
+ ImageOutputStream iosTest2 = ImageIO.createImageOutputStream(outputTest2);
+ TIFFUtilities.rotatePage(inputTest2, iosTest2, 90, 1);
+ iosTest2.close();
+
+ ImageInputStream checkTest2 = ImageIO.createImageInputStream(outputTest2);
+ reader.setInput(checkTest2);
+ for (int i = 0; i < 3; i++) {
+ Node metaData = reader.getImageMetadata(i)
+ .getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME);
+ short orientation = ((Number) expression.evaluate(metaData, XPathConstants.NUMBER)).shortValue();
+ Assert.assertEquals(orientation, i == 1
+ ? TIFFExtension.ORIENTATION_BOTRIGHT
+ : TIFFExtension.ORIENTATION_RIGHTTOP);
+ }
+ checkTest2.close();
+ }
+
+ @Test
+ public void testApplyOrientation() throws IOException {
+ InputStream inputStream = getClassLoaderResource("/contrib/tiff/multipage.tif").openStream();
+ File inputFile = File.createTempFile("imageiotest", "tif");
+ byte[] data = FileUtil.read(inputStream);
+ FileUtil.write(inputFile, data);
+ inputStream.close();
+
+ BufferedImage image = ImageIO.read(inputFile);
+
+ // rotate by 90�
+ BufferedImage image90 = TIFFUtilities.applyOrientation(image, TIFFExtension.ORIENTATION_RIGHTTOP);
+ // rotate by 270�
+ BufferedImage image360 = TIFFUtilities.applyOrientation(image90, TIFFExtension.ORIENTATION_LEFTBOT);
+
+ byte[] original = ((DataBufferByte) image.getData().getDataBuffer()).getData();
+ byte[] rotated = ((DataBufferByte) image360.getData().getDataBuffer()).getData();
+
+ Assert.assertArrayEquals(original, rotated);
+ }
+
+ protected URL getClassLoaderResource(final String pName) {
+ return getClass().getResource(pName);
+ }
+}
diff --git a/contrib/src/test/resources/contrib/tiff/multipage.tif b/contrib/src/test/resources/contrib/tiff/multipage.tif
new file mode 100644
index 00000000..2579cb34
Binary files /dev/null and b/contrib/src/test/resources/contrib/tiff/multipage.tif differ
diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFBaseline.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFBaseline.java
index 6d9a8cee..3d62fd60 100644
--- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFBaseline.java
+++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFBaseline.java
@@ -35,7 +35,7 @@ package com.twelvemonkeys.imageio.plugins.tiff;
* @author last modified by $Author: haraldk$
* @version $Id: TIFFBaseline.java,v 1.0 08.05.12 16:43 haraldk Exp$
*/
-interface TIFFBaseline {
+public interface TIFFBaseline {
int COMPRESSION_NONE = 1;
int COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE = 2;
int COMPRESSION_PACKBITS = 32773;
diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java
index f6444464..76e1f7a0 100644
--- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java
+++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java
@@ -35,7 +35,7 @@ package com.twelvemonkeys.imageio.plugins.tiff;
* @author last modified by $Author: haraldk$
* @version $Id: TIFFExtension.java,v 1.0 08.05.12 16:45 haraldk Exp$
*/
-interface TIFFExtension {
+public interface TIFFExtension {
/** CCITT T.4/Group 3 Fax compression. */
int COMPRESSION_CCITT_T4 = 3;
/** CCITT T.6/Group 4 Fax compression. */
diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java
index db5f9d5b..c06b8748 100644
--- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java
+++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java
@@ -113,7 +113,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
// TODO: Allow appending/partly overwrite of existing file...
}
- static final class TIFFEntry extends AbstractEntry {
+ public static final class TIFFEntry extends AbstractEntry {
// TODO: Expose a merge of this and the EXIFEntry class...
private final short type;
@@ -164,7 +164,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
throw new UnsupportedOperationException(String.format("Method guessType not implemented for value of type %s", value.getClass()));
}
- TIFFEntry(final int identifier, final Object value) {
+ public TIFFEntry(final int identifier, final Object value) {
this(identifier, guessType(value), value);
}