diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java
index 99f6b524..1b761e5a 100644
--- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java
+++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java
@@ -174,10 +174,57 @@ public abstract class AbstractEntry implements Entry {
AbstractEntry other = (AbstractEntry) pOther;
return identifier.equals(other.identifier) && (
- value == null && other.value == null || value != null && value.equals(other.value)
+ value == null && other.value == null || value != null && valueEquals(other)
);
}
+ private boolean valueEquals(final AbstractEntry other) {
+ return value.getClass().isArray() ? arrayEquals(value, other.value) : value.equals(other.value);
+ }
+
+ static boolean arrayEquals(final Object thisArray, final Object otherArray) {
+ // TODO: This is likely a utility method, and should be extracted
+ if (thisArray == otherArray) {
+ return true;
+ }
+ if (otherArray == null || thisArray == null || thisArray.getClass() != otherArray.getClass()) {
+ return false;
+ }
+
+ Class> componentType = thisArray.getClass().getComponentType();
+
+ if (componentType.isPrimitive()) {
+ if (thisArray instanceof byte[]) {
+ return Arrays.equals((byte[]) thisArray, (byte[]) otherArray);
+ }
+ if (thisArray instanceof char[]) {
+ return Arrays.equals((char[]) thisArray, (char[]) otherArray);
+ }
+ if (thisArray instanceof short[]) {
+ return Arrays.equals((short[]) thisArray, (short[]) otherArray);
+ }
+ if (thisArray instanceof int[]) {
+ return Arrays.equals((int[]) thisArray, (int[]) otherArray);
+ }
+ if (thisArray instanceof long[]) {
+ return Arrays.equals((long[]) thisArray, (long[]) otherArray);
+ }
+ if (thisArray instanceof boolean[]) {
+ return Arrays.equals((boolean[]) thisArray, (boolean[]) otherArray);
+ }
+ if (thisArray instanceof float[]) {
+ return Arrays.equals((float[]) thisArray, (float[]) otherArray);
+ }
+ if (thisArray instanceof double[]) {
+ return Arrays.equals((double[]) thisArray, (double[]) otherArray);
+ }
+
+ throw new AssertionError("Unsupported type:" + componentType);
+ }
+
+ return Arrays.equals((Object[]) thisArray, (Object[]) otherArray);
+ }
+
@Override
public String toString() {
String name = getFieldName();
diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java
index 7826f21e..ac13be42 100644
--- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java
+++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java
@@ -290,6 +290,8 @@ public final class EXIFReader extends MetadataReader {
private static Object readValue(final ImageInputStream pInput, final short pType, final int pCount) throws IOException {
// TODO: Review value "widening" for the unsigned types. Right now it's inconsistent. Should we leave it to client code?
+ // TODO: New strategy: Leave data as is, instead perform the widening in EXIFEntry.getValue.
+ // TODO: Add getValueByte/getValueUnsignedByte/getValueShort/getValueUnsignedShort/getValueInt/etc... in API.
long pos = pInput.getStreamPosition();
diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriter.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriter.java
new file mode 100644
index 00000000..1983ac04
--- /dev/null
+++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriter.java
@@ -0,0 +1,412 @@
+/*
+ * 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.imageio.metadata.exif;
+
+import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
+import com.twelvemonkeys.imageio.metadata.Directory;
+import com.twelvemonkeys.imageio.metadata.Entry;
+import com.twelvemonkeys.lang.Validate;
+
+import javax.imageio.IIOException;
+import javax.imageio.stream.ImageOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.util.*;
+
+/**
+ * EXIFWriter
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: EXIFWriter.java,v 1.0 17.07.13 10:20 haraldk Exp$
+ */
+public class EXIFWriter {
+
+ static final int WORD_LENGTH = 2;
+ static final int LONGWORD_LENGTH = 4;
+ static final int ENTRY_LENGTH = 12;
+
+ public boolean write(final Collection entries, final ImageOutputStream stream) throws IOException {
+ return write(new IFD(entries), stream);
+ }
+
+ public boolean write(final Directory directory, final ImageOutputStream stream) throws IOException {
+ Validate.notNull(directory);
+ Validate.notNull(stream);
+
+ // TODO: Should probably validate that the directory contains only valid TIFF entries...
+ // the writer will crash on non-Integer ids and unsupported types
+ // TODO: Implement the above validation in IFD constructor?
+
+ writeTIFFHeader(stream);
+
+ if (directory instanceof CompoundDirectory) {
+ CompoundDirectory compoundDirectory = (CompoundDirectory) directory;
+
+ for (int i = 0; i < compoundDirectory.directoryCount(); i++) {
+ writeIFD(compoundDirectory.getDirectory(i), stream, false);
+ }
+ }
+ else {
+ writeIFD(directory, stream, false);
+ }
+
+ // Offset to next IFD (EOF)
+ stream.writeInt(0);
+
+ return true;
+ }
+
+ public void writeTIFFHeader(final ImageOutputStream stream) throws IOException {
+ // Header
+ ByteOrder byteOrder = stream.getByteOrder();
+ stream.writeShort(byteOrder == ByteOrder.BIG_ENDIAN ? TIFF.BYTE_ORDER_MARK_BIG_ENDIAN : TIFF.BYTE_ORDER_MARK_LITTLE_ENDIAN);
+ stream.writeShort(42);
+ }
+
+ public long writeIFD(final Collection entries, ImageOutputStream stream) throws IOException {
+ return writeIFD(new IFD(entries), stream, false);
+ }
+
+ private long writeIFD(final Directory original, ImageOutputStream stream, boolean isSubIFD) throws IOException {
+ // TIFF spec says tags should be in increasing order, enforce that when writing
+ Directory ordered = ensureOrderedDirectory(original);
+
+ // Compute space needed for extra storage first, then write the offset to the IFD, so that the layout is:
+ // IFD offset
+ //
+ // IFD entries (values/offsets)
+ long dataOffset = stream.getStreamPosition();
+ long dataSize = computeDataSize(ordered);
+
+ // Offset to this IFD
+ final long ifdOffset = stream.getStreamPosition() + dataSize + LONGWORD_LENGTH;
+
+ if (!isSubIFD) {
+ stream.writeInt(assertIntegerOffset(ifdOffset));
+ dataOffset += LONGWORD_LENGTH;
+
+ // Seek to offset
+ stream.seek(ifdOffset);
+ }
+ else {
+ dataOffset += WORD_LENGTH + ordered.size() * ENTRY_LENGTH;
+ }
+
+ // Write directory
+ stream.writeShort(ordered.size());
+
+ for (Entry entry : ordered) {
+ // Write tag id
+ stream.writeShort((Integer) entry.getIdentifier());
+ // Write tag type
+ stream.writeShort(getType(entry));
+ // Write value count
+ stream.writeInt(getCount(entry));
+
+ // Write value
+ if (entry.getValue() instanceof Directory) {
+ // TODO: This could possibly be a compound directory, in which case the count should be > 1
+ stream.writeInt(assertIntegerOffset(dataOffset));
+ long streamPosition = stream.getStreamPosition();
+ stream.seek(dataOffset);
+ Directory subIFD = (Directory) entry.getValue();
+ writeIFD(subIFD, stream, true);
+ dataOffset += computeDataSize(subIFD);
+ stream.seek(streamPosition);
+ }
+ else {
+ dataOffset += writeValue(entry, dataOffset, stream);
+ }
+ }
+
+ return ifdOffset;
+ }
+
+ public long computeIFDSize(final Collection directory) {
+ return WORD_LENGTH + computeDataSize(new IFD(directory)) + directory.size() * ENTRY_LENGTH;
+ }
+
+ private long computeDataSize(final Directory directory) {
+ long dataSize = 0;
+
+ for (Entry entry : directory) {
+ int length = EXIFReader.getValueLength(getType(entry), getCount(entry));
+
+ if (length < 0) {
+ throw new IllegalArgumentException(String.format("Unknown size for entry %s", entry));
+ }
+
+ if (length > LONGWORD_LENGTH) {
+ dataSize += length;
+ }
+
+ if (entry.getValue() instanceof Directory) {
+ Directory subIFD = (Directory) entry.getValue();
+ long subIFDSize = WORD_LENGTH + subIFD.size() * ENTRY_LENGTH + computeDataSize(subIFD);
+ dataSize += subIFDSize;
+ }
+ }
+
+ return dataSize;
+ }
+
+ private Directory ensureOrderedDirectory(final Directory directory) {
+ if (!isSorted(directory)) {
+ List entries = new ArrayList(directory.size());
+
+ for (Entry entry : directory) {
+ entries.add(entry);
+ }
+
+ Collections.sort(entries, new Comparator() {
+ public int compare(Entry left, Entry right) {
+ return (Integer) left.getIdentifier() - (Integer) right.getIdentifier();
+ }
+ });
+
+ return new IFD(entries);
+ }
+
+ return directory;
+ }
+
+ private boolean isSorted(final Directory directory) {
+ int lastTag = 0;
+
+ for (Entry entry : directory) {
+ int tag = ((Integer) entry.getIdentifier()) & 0xffff;
+
+ if (tag < lastTag) {
+ return false;
+ }
+
+ lastTag = tag;
+ }
+
+ return true;
+ }
+
+ private long writeValue(Entry entry, long dataOffset, ImageOutputStream stream) throws IOException {
+ short type = getType(entry);
+ int valueLength = EXIFReader.getValueLength(type, getCount(entry));
+
+ if (valueLength <= LONGWORD_LENGTH) {
+ writeValueInline(entry.getValue(), type, stream);
+
+ // Pad
+ for (int i = valueLength; i < LONGWORD_LENGTH; i++) {
+ stream.write(0);
+ }
+
+ return 0;
+ }
+ else {
+ writeValueAt(dataOffset, entry.getValue(), type, stream);
+
+ return valueLength;
+ }
+ }
+
+ private int getCount(Entry entry) {
+ Object value = entry.getValue();
+ return value instanceof String ? ((String) value).getBytes(Charset.forName("UTF-8")).length + 1 : entry.valueCount();
+ }
+
+ private void writeValueInline(Object value, short type, ImageOutputStream stream) throws IOException {
+ if (value.getClass().isArray()) {
+ switch (type) {
+ case TIFF.TYPE_BYTE:
+ stream.write((byte[]) value);
+ break;
+ case TIFF.TYPE_SHORT:
+ short[] shorts;
+
+ if (value instanceof short[]) {
+ shorts = (short[]) value;
+ }
+ else if (value instanceof int[]) {
+ int[] ints = (int[]) value;
+ shorts = new short[ints.length];
+
+ for (int i = 0; i < ints.length; i++) {
+ shorts[i] = (short) ints[i];
+ }
+
+ }
+ else if (value instanceof long[]) {
+ long[] longs = (long[]) value;
+ shorts = new short[longs.length];
+
+ for (int i = 0; i < longs.length; i++) {
+ shorts[i] = (short) longs[i];
+ }
+ }
+ else {
+ throw new IllegalArgumentException("Unsupported type for TIFF SHORT: " + value.getClass());
+ }
+
+ stream.writeShorts(shorts, 0, shorts.length);
+ break;
+ case TIFF.TYPE_LONG:
+ int[] ints;
+
+ if (value instanceof int[]) {
+ ints = (int[]) value;
+ }
+ else if (value instanceof long[]) {
+ long[] longs = (long[]) value;
+ ints = new int[longs.length];
+
+ for (int i = 0; i < longs.length; i++) {
+ ints[i] = (int) longs[i];
+ }
+ }
+ else {
+ throw new IllegalArgumentException("Unsupported type for TIFF SHORT: " + value.getClass());
+ }
+
+ stream.writeInts(ints, 0, ints.length);
+
+ break;
+
+ case TIFF.TYPE_RATIONAL:
+ Rational[] rationals = (Rational[]) value;
+ for (Rational rational : rationals) {
+ stream.writeInt((int) rational.numerator());
+ stream.writeInt((int) rational.denominator());
+ }
+
+ // TODO: More types
+
+ default:
+ throw new IllegalArgumentException("Unsupported TIFF type: " + type);
+ }
+ }
+// else if (value instanceof Directory) {
+// writeIFD((Directory) value, stream, false);
+// }
+ else {
+ switch (type) {
+ case TIFF.TYPE_BYTE:
+ stream.writeByte((Integer) value);
+ break;
+ case TIFF.TYPE_ASCII:
+ byte[] bytes = ((String) value).getBytes(Charset.forName("UTF-8"));
+ stream.write(bytes);
+ stream.write(0);
+ break;
+ case TIFF.TYPE_SHORT:
+ stream.writeShort((Integer) value);
+ break;
+ case TIFF.TYPE_LONG:
+ stream.writeInt(((Number) value).intValue());
+ break;
+ case TIFF.TYPE_RATIONAL:
+ Rational rational = (Rational) value;
+ stream.writeInt((int) rational.numerator());
+ stream.writeInt((int) rational.denominator());
+ break;
+ // TODO: More types
+
+ default:
+ throw new IllegalArgumentException("Unsupported TIFF type: " + type);
+ }
+ }
+ }
+
+ private void writeValueAt(long dataOffset, Object value, short type, ImageOutputStream stream) throws IOException {
+ stream.writeInt(assertIntegerOffset(dataOffset));
+ long position = stream.getStreamPosition();
+ stream.seek(dataOffset);
+ writeValueInline(value, type, stream);
+ stream.seek(position);
+ }
+
+ private short getType(Entry entry) {
+ if (entry instanceof EXIFEntry) {
+ EXIFEntry exifEntry = (EXIFEntry) entry;
+ return exifEntry.getType();
+ }
+
+ Object value = Validate.notNull(entry.getValue());
+
+ boolean array = value.getClass().isArray();
+ if (array) {
+ value = Array.get(value, 0);
+ }
+
+ // Note: This "narrowing" is to keep data consistent between read/write.
+ // TODO: Check for negative values and use signed types?
+ if (value instanceof Byte) {
+ return TIFF.TYPE_BYTE;
+ }
+ if (value instanceof Short) {
+ if (!array && (Short) value < Byte.MAX_VALUE) {
+ return TIFF.TYPE_BYTE;
+ }
+
+ return TIFF.TYPE_SHORT;
+ }
+ if (value instanceof Integer) {
+ if (!array && (Integer) value < Short.MAX_VALUE) {
+ return TIFF.TYPE_SHORT;
+ }
+
+ return TIFF.TYPE_LONG;
+ }
+ if (value instanceof Long) {
+ if (!array && (Long) value < Integer.MAX_VALUE) {
+ return TIFF.TYPE_LONG;
+ }
+ }
+
+ if (value instanceof Rational) {
+ return TIFF.TYPE_RATIONAL;
+ }
+
+ if (value instanceof String) {
+ return TIFF.TYPE_ASCII;
+ }
+
+ // TODO: More types
+
+ throw new UnsupportedOperationException(String.format("Method getType not implemented for entry of type %s/value of type %s", entry.getClass(), value.getClass()));
+ }
+
+ private int assertIntegerOffset(long offset) throws IIOException {
+ if (offset > Integer.MAX_VALUE - (long) Integer.MIN_VALUE) {
+ throw new IIOException("Integer overflow for TIFF stream");
+ }
+
+ return (int) offset;
+ }
+}
diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/MetadataReaderAbstractTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/MetadataReaderAbstractTest.java
index 6f220006..189d6c28 100644
--- a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/MetadataReaderAbstractTest.java
+++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/MetadataReaderAbstractTest.java
@@ -40,9 +40,8 @@ import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
-import java.util.Arrays;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertNotNull;
/**
* ReaderAbstractTest
@@ -54,6 +53,7 @@ import static org.junit.Assert.*;
public abstract class MetadataReaderAbstractTest {
static {
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
+ ImageIO.setUseCache(false);
}
protected final URL getResource(final String name) throws IOException {
@@ -96,46 +96,7 @@ public abstract class MetadataReaderAbstractTest {
}
private static boolean valueEquals(final Object expected, final Object actual) {
- return expected.getClass().isArray() ? arrayEquals(expected, actual) : expected.equals(actual);
- }
-
- private static boolean arrayEquals(final Object expected, final Object actual) {
- Class> componentType = expected.getClass().getComponentType();
-
- if (actual == null || !actual.getClass().isArray() || actual.getClass().getComponentType() != componentType) {
- return false;
- }
-
- return componentType.isPrimitive() ? primitiveArrayEquals(componentType, expected, actual) : Arrays.equals((Object[]) expected, (Object[]) actual);
- }
-
- private static boolean primitiveArrayEquals(Class> componentType, Object expected, Object actual) {
- if (componentType == boolean.class) {
- return Arrays.equals((boolean[]) expected, (boolean[]) actual);
- }
- else if (componentType == byte.class) {
- return Arrays.equals((byte[]) expected, (byte[]) actual);
- }
- else if (componentType == char.class) {
- return Arrays.equals((char[]) expected, (char[]) actual);
- }
- else if (componentType == double.class) {
- return Arrays.equals((double[]) expected, (double[]) actual);
- }
- else if (componentType == float.class) {
- return Arrays.equals((float[]) expected, (float[]) actual);
- }
- else if (componentType == int.class) {
- return Arrays.equals((int[]) expected, (int[]) actual);
- }
- else if (componentType == long.class) {
- return Arrays.equals((long[]) expected, (long[]) actual);
- }
- else if (componentType == short.class) {
- return Arrays.equals((short[]) expected, (short[]) actual);
- }
-
- throw new AssertionError("Unsupported type:" + componentType);
+ return expected.getClass().isArray() ? AbstractEntry.arrayEquals(expected, actual) : expected.equals(actual);
}
public void describeTo(final Description description) {
diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriterTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriterTest.java
new file mode 100644
index 00000000..a425a7c7
--- /dev/null
+++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriterTest.java
@@ -0,0 +1,312 @@
+/*
+ * 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.imageio.metadata.exif;
+
+import com.twelvemonkeys.imageio.metadata.AbstractDirectory;
+import com.twelvemonkeys.imageio.metadata.AbstractEntry;
+import com.twelvemonkeys.imageio.metadata.Directory;
+import com.twelvemonkeys.imageio.metadata.Entry;
+import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
+import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi;
+import com.twelvemonkeys.io.FastByteArrayOutputStream;
+import org.junit.Test;
+
+import javax.imageio.ImageIO;
+import javax.imageio.spi.IIORegistry;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
+import javax.imageio.stream.ImageOutputStreamImpl;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * EXIFWriterTest
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: EXIFWriterTest.java,v 1.0 18.07.13 09:53 haraldk Exp$
+ */
+public class EXIFWriterTest {
+ static {
+ IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
+ ImageIO.setUseCache(false);
+ }
+
+ protected final URL getResource(final String name) throws IOException {
+ return getClass().getResource(name);
+ }
+
+ protected final ImageInputStream getDataAsIIS() throws IOException {
+ return ImageIO.createImageInputStream(getData());
+ }
+
+ // @Override
+ protected InputStream getData() throws IOException {
+ return getResource("/exif/exif-jpeg-segment.bin").openStream();
+ }
+
+// @Override
+ protected EXIFReader createReader() {
+ return new EXIFReader();
+ }
+
+ protected EXIFWriter createWriter() {
+ return new EXIFWriter();
+ }
+
+ @Test
+ public void testWriteReadSimple() throws IOException {
+ ArrayList entries = new ArrayList();
+ entries.add(new EXIFEntry(TIFF.TAG_ORIENTATION, 1, TIFF.TYPE_SHORT));
+ entries.add(new EXIFEntry(TIFF.TAG_IMAGE_WIDTH, 1600, TIFF.TYPE_SHORT));
+ entries.add(new AbstractEntry(TIFF.TAG_IMAGE_HEIGHT, 900) {});
+ entries.add(new EXIFEntry(TIFF.TAG_ARTIST, "Harald K.", TIFF.TYPE_ASCII));
+ entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {});
+ Directory directory = new AbstractDirectory(entries) {};
+
+ ByteArrayOutputStream output = new FastByteArrayOutputStream(1024);
+ ImageOutputStream imageStream = ImageIO.createImageOutputStream(output);
+ new EXIFWriter().write(directory, imageStream);
+ imageStream.flush();
+
+ assertEquals(output.size(), imageStream.getStreamPosition());
+
+ byte[] data = output.toByteArray();
+
+ assertEquals(106, data.length);
+ assertEquals('M', data[0]);
+ assertEquals('M', data[1]);
+ assertEquals(0, data[2]);
+ assertEquals(42, data[3]);
+
+ Directory read = new EXIFReader().read(new ByteArrayImageInputStream(data));
+
+ assertNotNull(read);
+ assertEquals(5, read.size());
+
+ // TODO: Assert that the tags are written in ascending order (don't test the read directory, but the file structure)!
+
+ assertNotNull(read.getEntryById(TIFF.TAG_SOFTWARE));
+ assertEquals("TwelveMonkeys ImageIO", read.getEntryById(TIFF.TAG_SOFTWARE).getValue());
+
+ assertNotNull(read.getEntryById(TIFF.TAG_IMAGE_WIDTH));
+ assertEquals(1600, read.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue());
+
+ assertNotNull(read.getEntryById(TIFF.TAG_IMAGE_HEIGHT));
+ assertEquals(900, read.getEntryById(TIFF.TAG_IMAGE_HEIGHT).getValue());
+
+ assertNotNull(read.getEntryById(TIFF.TAG_ORIENTATION));
+ assertEquals(1, read.getEntryById(TIFF.TAG_ORIENTATION).getValue());
+
+ assertNotNull(read.getEntryById(TIFF.TAG_ARTIST));
+ assertEquals("Harald K.", read.getEntryById(TIFF.TAG_ARTIST).getValue());
+ }
+
+ @Test
+ public void testWriteMotorola() throws IOException {
+ ArrayList entries = new ArrayList();
+ entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {});
+ entries.add(new EXIFEntry(TIFF.TAG_IMAGE_WIDTH, Integer.MAX_VALUE, TIFF.TYPE_LONG));
+ Directory directory = new AbstractDirectory(entries) {};
+
+ ByteArrayOutputStream output = new FastByteArrayOutputStream(1024);
+ ImageOutputStream imageStream = ImageIO.createImageOutputStream(output);
+
+ imageStream.setByteOrder(ByteOrder.BIG_ENDIAN); // BE = Motorola
+
+ new EXIFWriter().write(directory, imageStream);
+ imageStream.flush();
+
+ assertEquals(output.size(), imageStream.getStreamPosition());
+
+ byte[] data = output.toByteArray();
+
+ assertEquals(60, data.length);
+ assertEquals('M', data[0]);
+ assertEquals('M', data[1]);
+ assertEquals(0, data[2]);
+ assertEquals(42, data[3]);
+
+ Directory read = new EXIFReader().read(new ByteArrayImageInputStream(data));
+
+ assertNotNull(read);
+ assertEquals(2, read.size());
+ assertNotNull(read.getEntryById(TIFF.TAG_SOFTWARE));
+ assertEquals("TwelveMonkeys ImageIO", read.getEntryById(TIFF.TAG_SOFTWARE).getValue());
+ assertNotNull(read.getEntryById(TIFF.TAG_IMAGE_WIDTH));
+ assertEquals((long) Integer.MAX_VALUE, read.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue());
+ }
+
+ @Test
+ public void testWriteIntel() throws IOException {
+ ArrayList entries = new ArrayList();
+ entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {});
+ entries.add(new EXIFEntry(TIFF.TAG_IMAGE_WIDTH, Integer.MAX_VALUE, TIFF.TYPE_LONG));
+ Directory directory = new AbstractDirectory(entries) {};
+
+ ByteArrayOutputStream output = new FastByteArrayOutputStream(1024);
+ ImageOutputStream imageStream = ImageIO.createImageOutputStream(output);
+
+ imageStream.setByteOrder(ByteOrder.LITTLE_ENDIAN); // LE = Intel
+
+ new EXIFWriter().write(directory, imageStream);
+ imageStream.flush();
+
+ assertEquals(output.size(), imageStream.getStreamPosition());
+
+ byte[] data = output.toByteArray();
+
+ assertEquals(60, data.length);
+ assertEquals('I', data[0]);
+ assertEquals('I', data[1]);
+ assertEquals(42, data[2]);
+ assertEquals(0, data[3]);
+
+ Directory read = new EXIFReader().read(new ByteArrayImageInputStream(data));
+
+ assertNotNull(read);
+ assertEquals(2, read.size());
+ assertNotNull(read.getEntryById(TIFF.TAG_SOFTWARE));
+ assertEquals("TwelveMonkeys ImageIO", read.getEntryById(TIFF.TAG_SOFTWARE).getValue());
+ assertNotNull(read.getEntryById(TIFF.TAG_IMAGE_WIDTH));
+ assertEquals((long) Integer.MAX_VALUE, read.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue());
+ }
+
+ @Test
+ public void testNesting() throws IOException {
+ EXIFEntry artist = new EXIFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO", TIFF.TYPE_ASCII);
+
+ EXIFEntry subSubSubSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(artist)), TIFF.TYPE_LONG);
+ EXIFEntry subSubSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubSubSubIFD)), TIFF.TYPE_LONG);
+ EXIFEntry subSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubSubIFD)), TIFF.TYPE_LONG);
+ EXIFEntry subIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubIFD)), TIFF.TYPE_LONG);
+
+ Directory directory = new IFD(Collections.singletonList(subIFD));
+
+ ByteArrayOutputStream output = new FastByteArrayOutputStream(1024);
+ ImageOutputStream imageStream = ImageIO.createImageOutputStream(output);
+
+ new EXIFWriter().write(directory, imageStream);
+ imageStream.flush();
+
+ assertEquals(output.size(), imageStream.getStreamPosition());
+
+ Directory read = new EXIFReader().read(new ByteArrayImageInputStream(output.toByteArray()));
+
+ assertNotNull(read);
+ assertEquals(1, read.size());
+ assertEquals(subIFD, read.getEntryById(TIFF.TAG_SUB_IFD)); // Recursively tests content!
+ }
+
+ @Test
+ public void testReadWriteRead() throws IOException {
+ Directory original = createReader().read(getDataAsIIS());
+
+ ByteArrayOutputStream output = new FastByteArrayOutputStream(256);
+ ImageOutputStream imageOutput = ImageIO.createImageOutputStream(output);
+
+ try {
+ createWriter().write(original, imageOutput);
+ }
+ finally {
+ imageOutput.close();
+ }
+
+ Directory read = createReader().read(new ByteArrayImageInputStream(output.toByteArray()));
+
+ assertEquals(original, read);
+ }
+
+ @Test
+ public void testComputeIFDSize() throws IOException {
+ ArrayList entries = new ArrayList();
+ entries.add(new EXIFEntry(TIFF.TAG_ORIENTATION, 1, TIFF.TYPE_SHORT));
+ entries.add(new EXIFEntry(TIFF.TAG_IMAGE_WIDTH, 1600, TIFF.TYPE_SHORT));
+ entries.add(new AbstractEntry(TIFF.TAG_IMAGE_HEIGHT, 900) {});
+ entries.add(new EXIFEntry(TIFF.TAG_ARTIST, "Harald K.", TIFF.TYPE_ASCII));
+ entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {});
+
+ EXIFWriter writer = createWriter();
+
+ ImageOutputStream stream = new NullImageOutputStream();
+ writer.write(new IFD(entries), stream);
+
+ assertEquals(stream.getStreamPosition(), writer.computeIFDSize(entries) + 12);
+ }
+
+ @Test
+ public void testComputeIFDSizeNested() throws IOException {
+ EXIFEntry artist = new EXIFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO", TIFF.TYPE_ASCII);
+
+ EXIFEntry subSubSubSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(artist)), TIFF.TYPE_LONG);
+ EXIFEntry subSubSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubSubSubIFD)), TIFF.TYPE_LONG);
+ EXIFEntry subSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubSubIFD)), TIFF.TYPE_LONG);
+ EXIFEntry subIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubIFD)), TIFF.TYPE_LONG);
+
+ List entries = Collections.singletonList(subIFD);
+
+ EXIFWriter writer = createWriter();
+
+ ImageOutputStream stream = new NullImageOutputStream();
+ writer.write(new IFD(entries), stream);
+
+ assertEquals(stream.getStreamPosition(), writer.computeIFDSize(entries) + 12);
+ }
+
+ private static class NullImageOutputStream extends ImageOutputStreamImpl {
+ @Override
+ public void write(int b) throws IOException {
+ streamPos++;
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ streamPos += len;
+ }
+
+ @Override
+ public int read() throws IOException {
+ throw new UnsupportedOperationException("Method read not implemented");
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ throw new UnsupportedOperationException("Method read not implemented");
+ }
+ }
+}
diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDifferencingStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDifferencingStream.java
new file mode 100644
index 00000000..7a8943f1
--- /dev/null
+++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDifferencingStream.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (c) 2014, 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.imageio.plugins.tiff;
+
+import com.twelvemonkeys.lang.Validate;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.Channels;
+import java.nio.channels.WritableByteChannel;
+
+/**
+ * A decoder for data converted using "horizontal differencing predictor".
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: HorizontalDeDifferencingStream.java,v 1.0 11.03.13 14:20 haraldk Exp$
+ */
+final class HorizontalDifferencingStream extends OutputStream {
+ // See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
+
+ private final int columns;
+ // NOTE: PlanarConfiguration == 2 may be treated as samplesPerPixel == 1
+ private final int samplesPerPixel;
+ private final int bitsPerSample;
+
+ private final WritableByteChannel channel;
+ private final ByteBuffer buffer;
+
+ public HorizontalDifferencingStream(final OutputStream stream, final int columns, final int samplesPerPixel, final int bitsPerSample, final ByteOrder byteOrder) {
+ this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0");
+ this.samplesPerPixel = Validate.isTrue(bitsPerSample >= 8 || samplesPerPixel == 1, samplesPerPixel, "Unsupported samples per pixel for < 8 bit samples: %s");
+ this.bitsPerSample = Validate.isTrue(isValidBPS(bitsPerSample), bitsPerSample, "Unsupported bits per sample value: %s");
+
+ channel = Channels.newChannel(Validate.notNull(stream, "stream"));
+
+ buffer = ByteBuffer.allocate((columns * samplesPerPixel * bitsPerSample + 7) / 8).order(byteOrder);
+ }
+
+ private boolean isValidBPS(final int bitsPerSample) {
+ switch (bitsPerSample) {
+ case 1:
+ case 2:
+ case 4:
+ case 8:
+ case 16:
+ case 32:
+ case 64:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private boolean flushBuffer() throws IOException {
+ if (buffer.position() == 0) {
+ return false;
+ }
+
+ encodeRow();
+
+ buffer.flip();
+ channel.write(buffer);
+ buffer.clear();
+
+ return true;
+ }
+
+ private void encodeRow() throws EOFException {
+ // Apply horizontal predictor
+ byte original;
+ int sample = 0;
+ int prev;
+ byte temp;
+
+ // Optimization:
+ // Access array directly for <= 8 bits per sample, as buffer does extra index bounds check for every
+ // put/get operation... (Measures to about 100 ms difference for 4000 x 3000 image)
+ final byte[] array = buffer.array();
+
+ switch (bitsPerSample) {
+ case 1:
+ for (int b = ((columns + 7) / 8) - 1; b > 0; b--) {
+ // Subtract previous sample from current sample
+ original = array[b];
+ prev = array[b - 1] & 0x1;
+ temp = (byte) ((((original & 0x80) >> 7) - prev) << 7);
+
+ sample = ((original & 0x40) >> 6) - ((original & 0x80) >> 7);
+ temp |= (sample << 6) & 0x40;
+
+ sample = ((original & 0x20) >> 5) - ((original & 0x40) >> 6);
+ temp |= (sample << 5) & 0x20;
+
+ sample = ((original & 0x10) >> 4) - ((original & 0x20) >> 5);
+ temp |= (sample << 4) & 0x10;
+
+ sample = ((original & 0x08) >> 3) - ((original & 0x10) >> 4);
+ temp |= (sample << 3) & 0x08;
+
+ sample = ((original & 0x04) >> 2) - ((original & 0x08) >> 3);
+ temp |= (sample << 2) & 0x04;
+
+ sample = ((original & 0x02) >> 1) - ((original & 0x04) >> 2);
+ temp |= (sample << 1) & 0x02;
+
+ sample = (original & 0x01) - ((original & 0x02) >> 1);
+
+ array[b] = (byte) (temp & 0xfe | sample & 0x01);
+ }
+
+ // First sample in row as is
+ original = array[0];
+ temp = (byte) (original & 0x80);
+
+ sample = ((original & 0x40) >> 6) - ((original & 0x80) >> 7);
+ temp |= (sample << 6) & 0x40;
+
+ sample = ((original & 0x20) >> 5) - ((original & 0x40) >> 6);
+ temp |= (sample << 5) & 0x20;
+
+ sample = ((original & 0x10) >> 4) - ((original & 0x20) >> 5);
+ temp |= (sample << 4) & 0x10;
+
+ sample = ((original & 0x08) >> 3) - ((original & 0x10) >> 4);
+ temp |= (sample << 3) & 0x08;
+
+ sample = ((original & 0x04) >> 2) - ((original & 0x08) >> 3);
+ temp |= (sample << 2) & 0x04;
+
+ sample = ((original & 0x02) >> 1) - ((original & 0x04) >> 2);
+ temp |= (sample << 1) & 0x02;
+
+ sample = (original & 0x01) - ((original & 0x02) >> 1);
+
+ array[0] = (byte) (temp & 0xfe | sample & 0x01);
+ break;
+
+ case 2:
+ for (int b = ((columns + 3) / 4) - 1; b > 0; b--) {
+ // Subtract previous sample from current sample
+ original = array[b];
+ prev = array[b - 1] & 0x3;
+ temp = (byte) ((((original & 0xc0) >> 6) - prev) << 6);
+
+ sample = ((original & 0x30) >> 4) - ((original & 0xc0) >> 6);
+ temp |= (sample << 4) & 0x30;
+
+ sample = ((original & 0x0c) >> 2) - ((original & 0x30) >> 4);
+ temp |= (sample << 2) & 0x0c;
+
+ sample = (original & 0x03) - ((original & 0x0c) >> 2);
+
+ array[b] = (byte) (temp & 0xfc | sample & 0x03);
+ }
+
+ // First sample in row as is
+ original = array[0];
+ temp = (byte) (original & 0xc0);
+
+ sample = ((original & 0x30) >> 4) - ((original & 0xc0) >> 6);
+ temp |= (sample << 4) & 0x30;
+
+ sample = ((original & 0x0c) >> 2) - ((original & 0x30) >> 4);
+ temp |= (sample << 2) & 0x0c;
+
+ sample = (original & 0x03) - ((original & 0x0c) >> 2);
+
+ array[0] = (byte) (temp & 0xfc | sample & 0x03);
+ break;
+
+ case 4:
+ for (int b = ((columns + 1) / 2) - 1; b > 0; b--) {
+ // Subtract previous sample from current sample
+ original = array[b];
+ prev = array[b - 1] & 0xf;
+ temp = (byte) ((((original & 0xf0) >> 4) - prev) << 4);
+ sample = (original & 0x0f) - ((original & 0xf0) >> 4);
+ array[b] = (byte) (temp & 0xf0 | sample & 0xf);
+ }
+
+ // First sample in row as is
+ original = array[0];
+ sample = (original & 0x0f) - ((original & 0xf0) >> 4);
+ array[0] = (byte) (original & 0xf0 | sample & 0xf);
+
+ break;
+
+ case 8:
+ for (int x = columns - 1; x > 0; x--) {
+ final int xOff = x * samplesPerPixel;
+
+ for (int b = 0; b < samplesPerPixel; b++) {
+ int off = xOff + b;
+ array[off] = (byte) (array[off] - array[off - samplesPerPixel]);
+ }
+ }
+ break;
+
+ case 16:
+ for (int x = columns - 1; x > 0; x--) {
+ for (int b = 0; b < samplesPerPixel; b++) {
+ int off = x * samplesPerPixel + b;
+ buffer.putShort(2 * off, (short) (buffer.getShort(2 * off) - buffer.getShort(2 * (off - samplesPerPixel))));
+ }
+ }
+ break;
+
+ case 32:
+ for (int x = columns - 1; x > 0; x--) {
+ for (int b = 0; b < samplesPerPixel; b++) {
+ int off = x * samplesPerPixel + b;
+ buffer.putInt(4 * off, buffer.getInt(4 * off) - buffer.getInt(4 * (off - samplesPerPixel)));
+ }
+ }
+ break;
+
+ case 64:
+ for (int x = columns - 1; x > 0; x--) {
+ for (int b = 0; b < samplesPerPixel; b++) {
+ int off = x * samplesPerPixel + b;
+ buffer.putLong(8 * off, buffer.getLong(8 * off) - buffer.getLong(8 * (off - samplesPerPixel)));
+ }
+ }
+ break;
+
+ default:
+ throw new AssertionError(String.format("Unsupported bits per sample value: %d", bitsPerSample));
+ }
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ buffer.put((byte) b);
+
+ if (!buffer.hasRemaining()) {
+ flushBuffer();
+ }
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ while (len > 0) {
+ int maxLenForRow = Math.min(len, buffer.remaining());
+
+ buffer.put(b, off, maxLenForRow);
+ off += maxLenForRow;
+ len -= maxLenForRow;
+
+ if (!buffer.hasRemaining()) {
+ flushBuffer();
+ }
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ flushBuffer();
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ flushBuffer();
+ super.close();
+ }
+ finally {
+ if (channel.isOpen()) {
+ channel.close();
+ }
+ }
+ }
+}
diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java
index bc47635c..d444b799 100644
--- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java
+++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java
@@ -300,8 +300,8 @@ abstract class LZWDecoder implements Decoder {
this.previous = previous;
}
- public final LZWString concatenate(final byte firstChar) {
- return new LZWString(firstChar, this.firstChar, length + 1, this);
+ public final LZWString concatenate(final byte value) {
+ return new LZWString(value, this.firstChar, length + 1, this);
}
public final void writeTo(final ByteBuffer buffer) {
diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java
index 451993f6..5a74e5de 100644
--- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java
+++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java
@@ -58,15 +58,14 @@ public class TIFFImageReaderSpi extends ImageReaderSpi {
super(
providerInfo.getVendorName(),
providerInfo.getVersion(),
- new String[]{"tiff", "TIFF"},
- new String[]{"tif", "tiff"},
- new String[]{
+ new String[] {"tiff", "TIFF"},
+ new String[] {"tif", "tiff"},
+ new String[] {
"image/tiff", "image/x-tiff"
},
"com.twelvemkonkeys.imageio.plugins.tiff.TIFFImageReader",
new Class[] {ImageInputStream.class},
-// new String[]{"com.twelvemkonkeys.imageio.plugins.tif.TIFFImageWriterSpi"},
- null,
+ new String[] {"com.twelvemkonkeys.imageio.plugins.tif.TIFFImageWriterSpi"},
true, // supports standard stream metadata
null, null, // native stream format name and class
null, null, // extra stream formats
diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriteParam.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriteParam.java
new file mode 100644
index 00000000..69eba0c1
--- /dev/null
+++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriteParam.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2014, 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.imageio.plugins.tiff;
+
+import javax.imageio.ImageWriteParam;
+import java.util.Locale;
+
+/**
+ * TIFFImageWriteParam
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: TIFFImageWriteParam.java,v 1.0 18.09.13 12:47 haraldk Exp$
+ */
+public final class TIFFImageWriteParam extends ImageWriteParam {
+ // TODO: Support no compression (None/1)
+ // TODO: Support ZLIB (/Deflate) compression (8)
+ // TODO: Support PackBits compression (32773)
+ // TODO: Support JPEG compression (7)
+ // TODO: Support CCITT Modified Huffman compression (2)
+ // TODO: Support LZW compression (5)?
+ // TODO: Support JBIG compression via ImageIO plugin/delegate?
+ // TODO: Support JPEG2000 compression via ImageIO plugin/delegate?
+ // TODO: Support tiling
+ // TODO: Support predictor. See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
+
+ TIFFImageWriteParam() {
+ this(Locale.getDefault());
+ }
+
+ TIFFImageWriteParam(final Locale locale) {
+ super(locale);
+
+ // NOTE: We use the same spelling/casing as the JAI equivalent to be as compatible as possible
+ // See: http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/TIFFImageWriteParam.html
+ compressionTypes = new String[] {"None", /* "CCITT RLE", "CCITT T.4", "CCITT T.6", */ "LZW", "JPEG", "ZLib", "PackBits", "Deflate", /* "EXIF JPEG" */ };
+ compressionType = compressionTypes[0];
+ canWriteCompressed = true;
+ }
+
+ @Override
+ public float[] getCompressionQualityValues() {
+ super.getCompressionQualityValues();
+
+ // TODO: Special case for JPEG and ZLib/Deflate
+
+ return null;
+ }
+
+ @Override
+ public String[] getCompressionQualityDescriptions() {
+ super.getCompressionQualityDescriptions();
+
+ // TODO: Special case for JPEG and ZLib/Deflate
+
+ return null;
+ }
+
+ static int getCompressionType(final ImageWriteParam param) {
+ // TODO: Support mode COPY_FROM_METADATA (when we have metadata...)
+ if (param == null || param.getCompressionMode() != MODE_EXPLICIT || param.getCompressionType().equals("None")) {
+ return TIFFBaseline.COMPRESSION_NONE;
+ }
+ else if (param.getCompressionType().equals("PackBits")) {
+ return TIFFBaseline.COMPRESSION_PACKBITS;
+ }
+ else if (param.getCompressionType().equals("ZLib")) {
+ return TIFFExtension.COMPRESSION_ZLIB;
+ }
+ else if (param.getCompressionType().equals("Deflate")) {
+ return TIFFExtension.COMPRESSION_DEFLATE;
+ }
+ else if (param.getCompressionType().equals("LZW")) {
+ return TIFFExtension.COMPRESSION_LZW;
+ }
+ else if (param.getCompressionType().equals("JPEG")) {
+ return TIFFExtension.COMPRESSION_JPEG;
+ }
+
+ throw new IllegalArgumentException(String.format("Unsupported compression type: %s", param.getCompressionType()));
+ }
+}
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
new file mode 100644
index 00000000..9697c157
--- /dev/null
+++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java
@@ -0,0 +1,734 @@
+/*
+ * Copyright (c) 2014, 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.imageio.plugins.tiff;
+
+import com.twelvemonkeys.image.ImageUtil;
+import com.twelvemonkeys.imageio.ImageWriterBase;
+import com.twelvemonkeys.imageio.metadata.AbstractEntry;
+import com.twelvemonkeys.imageio.metadata.Entry;
+import com.twelvemonkeys.imageio.metadata.exif.EXIFWriter;
+import com.twelvemonkeys.imageio.metadata.exif.Rational;
+import com.twelvemonkeys.imageio.metadata.exif.TIFF;
+import com.twelvemonkeys.imageio.util.IIOUtil;
+import com.twelvemonkeys.io.enc.EncoderStream;
+import com.twelvemonkeys.io.enc.PackBitsEncoder;
+
+import javax.imageio.*;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.spi.ImageWriterSpi;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
+import java.awt.*;
+import java.awt.color.ColorSpace;
+import java.awt.image.*;
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+
+/**
+ * TIFFImageWriter
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: TIFFImageWriter.java,v 1.0 18.09.13 12:46 haraldk Exp$
+ */
+public final class TIFFImageWriter extends ImageWriterBase {
+ // Short term
+ // TODO: Support JPEG compression (7) - might need extra input to allow multiple images with single DQT
+ // TODO: Use sensible defaults for compression based on input? None is sensible... :-)
+
+ // Long term
+ // TODO: Support tiling
+ // TODO: Support thumbnails
+ // TODO: Support ImageIO metadata
+ // TODO: Support CCITT Modified Huffman compression (2)
+ // TODO: Full "Baseline TIFF" support
+ // TODO: Support LZW compression (5)?
+ // ----
+ // TODO: Support storing multiple images in one stream (multi-page TIFF)
+ // TODO: Support use-case: Transcode multi-layer PSD to multi-page TIFF with metadata
+ // TODO: Support use-case: Transcode multi-page TIFF to multiple single-page TIFFs with metadata
+ // TODO: Support use-case: Losslessly transcode JPEG to JPEG in TIFF with (EXIF) metadata (and back)
+
+ // Very long term...
+ // TODO: Support JBIG compression via ImageIO plugin/delegate? Pending support in Reader
+ // TODO: Support JPEG2000 compression via ImageIO plugin/delegate? Pending support in Reader
+
+ // Done
+ // Create a basic writer that supports most inputs. Store them using the simplest possible format.
+ // Support no compression (None/1) - BASELINE
+ // Support predictor. See TIFF 6.0 Specification, Section 14: "Differencing Predictor", page 64.
+ // Support PackBits compression (32773) - easy - BASELINE
+ // Support ZLIB (/Deflate) compression (8) - easy
+
+ public static final Rational STANDARD_DPI = new Rational(72);
+
+ TIFFImageWriter(final ImageWriterSpi provider) {
+ super(provider);
+ }
+
+ static final class TIFFEntry extends AbstractEntry {
+ TIFFEntry(Object identifier, Object value) {
+ super(identifier, value);
+ }
+ }
+
+ @Override
+ public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException {
+ // TODO: Validate input
+
+ assertOutput();
+
+ // TODO: Consider writing TIFF header, offset to IFD0 (leave blank), write image data with correct
+ // tiling/compression/etc, then write IFD0, go back and update IFD0 offset?
+
+ // Write minimal TIFF header (required "Baseline" fields)
+ // Use EXIFWriter to write leading metadata (TODO: consider rename to TTIFFWriter, again...)
+ // TODO: Make TIFFEntry and possibly TIFFDirectory? public
+ RenderedImage renderedImage = image.getRenderedImage();
+ ColorModel colorModel = renderedImage.getColorModel();
+ int numComponents = colorModel.getNumComponents();
+
+ SampleModel sampleModel = renderedImage.getSampleModel();
+
+ int[] bandOffsets;
+ int[] bitOffsets;
+ if (sampleModel instanceof ComponentSampleModel) {
+ bandOffsets = ((ComponentSampleModel) sampleModel).getBandOffsets();
+// System.err.println("bandOffsets: " + Arrays.toString(bandOffsets));
+ bitOffsets = null;
+ }
+ else if (sampleModel instanceof SinglePixelPackedSampleModel) {
+ bitOffsets = ((SinglePixelPackedSampleModel) sampleModel).getBitOffsets();
+// System.err.println("bitOffsets: " + Arrays.toString(bitOffsets));
+ bandOffsets = null;
+ }
+ else if (sampleModel instanceof MultiPixelPackedSampleModel) {
+ bitOffsets = null;
+ bandOffsets = new int[] {0};
+ }
+ else {
+ throw new IllegalArgumentException("Unknown bit/bandOffsets for sample model: " + sampleModel);
+ }
+
+ List entries = new ArrayList();
+ entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, renderedImage.getWidth()));
+ entries.add(new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, renderedImage.getHeight()));
+// entries.add(new TIFFEntry(TIFF.TAG_ORIENTATION, 1)); // (optional)
+ entries.add(new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, asShortArray(sampleModel.getSampleSize())));
+ // If numComponents > 3, write ExtraSamples
+ if (numComponents > 3) {
+ // TODO: Write per component > 3
+ if (colorModel.hasAlpha()) {
+ entries.add(new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, colorModel.isAlphaPremultiplied() ? TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA : TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA));
+ }
+ else {
+ entries.add(new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED));
+ }
+ }
+ // Write compression field from param or metadata
+ int compression = TIFFImageWriteParam.getCompressionType(param);
+ entries.add(new TIFFEntry(TIFF.TAG_COMPRESSION, compression));
+ // TODO: Let param control
+ switch (compression) {
+ case TIFFExtension.COMPRESSION_ZLIB:
+ case TIFFExtension.COMPRESSION_DEFLATE:
+ case TIFFExtension.COMPRESSION_LZW:
+ entries.add(new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING));
+ default:
+ }
+
+ int photometric = getPhotometricInterpretation(colorModel);
+ entries.add(new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, photometric));
+
+ if (photometric == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) {
+ entries.add(new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel)));
+ entries.add(new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1));
+ }
+ else {
+ entries.add(new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numComponents));
+ }
+
+ if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT /* TODO: if (isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) {
+ entries.add(new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT));
+ }
+
+ entries.add(new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer")); // TODO: Get from metadata (optional) + fill in version number
+
+ entries.add(new TIFFEntry(TIFF.TAG_X_RESOLUTION, STANDARD_DPI));
+ entries.add(new TIFFEntry(TIFF.TAG_Y_RESOLUTION, STANDARD_DPI));
+ entries.add(new TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFFBaseline.RESOLUTION_UNIT_DPI));
+
+ // TODO: RowsPerStrip - can be entire image (or even 2^32 -1), but it's recommended to write "about 8K bytes" per strip
+ entries.add(new TIFFEntry(TIFF.TAG_ROWS_PER_STRIP, Integer.MAX_VALUE)); // TODO: Allowed but not recommended
+ // - StripByteCounts - for no compression, entire image data... (TODO: How to know the byte counts prior to writing data?)
+ TIFFEntry dummyStripByteCounts = new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, -1);
+ entries.add(dummyStripByteCounts); // Updated later
+ // - StripOffsets - can be offset to single strip only (TODO: but how large is the IFD data...???)
+ TIFFEntry dummyStripOffsets = new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, -1);
+ entries.add(dummyStripOffsets); // Updated later
+
+ // TODO: If tiled, write tile indexes etc, or always do that?
+
+ EXIFWriter exifWriter = new EXIFWriter();
+
+ if (compression == TIFFBaseline.COMPRESSION_NONE) {
+ // This implementation, allows semi-streaming-compatible uncompressed TIFFs
+ long streamOffset = exifWriter.computeIFDSize(entries) + 12; // 12 == 4 byte magic, 4 byte IDD 0 pointer, 4 byte EOF
+
+ entries.remove(dummyStripByteCounts);
+ entries.add(new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, renderedImage.getWidth() * renderedImage.getHeight() * numComponents));
+ entries.remove(dummyStripOffsets);
+ entries.add(new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, streamOffset));
+
+ exifWriter.write(entries, imageOutput); // NOTE: Writer takes case of ordering tags
+ imageOutput.flush();
+ }
+ else {
+ // Unless compression == 1 / COMPRESSION_NONE (and all offsets known), write only TIFF header/magic + leave room for IFD0 offset
+ exifWriter.writeTIFFHeader(imageOutput);
+ imageOutput.writeInt(-1); // IFD0 pointer, will be updated later
+ }
+
+ // TODO: Create compressor stream per Tile/Strip
+ // Write image data
+ writeImageData(createCompressorStream(renderedImage, param), renderedImage, numComponents, bandOffsets, bitOffsets);
+
+ // TODO: Update IFD0-pointer, and write IFD
+ if (compression != TIFFBaseline.COMPRESSION_NONE) {
+ long streamPosition = imageOutput.getStreamPosition();
+
+ entries.remove(dummyStripOffsets);
+ entries.add(new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, 8));
+ entries.remove(dummyStripByteCounts);
+ entries.add(new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, streamPosition - 8));
+
+ long ifdOffset = exifWriter.writeIFD(entries, imageOutput);
+ imageOutput.writeInt(0); // Next IFD (none)
+ streamPosition = imageOutput.getStreamPosition();
+
+ // Update IFD0 pointer
+ imageOutput.seek(4);
+ imageOutput.writeInt((int) ifdOffset);
+ imageOutput.seek(streamPosition);
+ imageOutput.flush();
+ }
+ }
+
+ private DataOutput createCompressorStream(RenderedImage image, ImageWriteParam param) {
+ /*
+ 36 MB test data:
+
+ No compression:
+ Write time: 450 ms
+ output.length: 36000226
+
+ PackBits:
+ Write time: 688 ms
+ output.length: 30322187
+
+ Deflate, BEST_SPEED (1):
+ Write time: 1276 ms
+ output.length: 14128866
+
+ Deflate, 2:
+ Write time: 1297 ms
+ output.length: 13848735
+
+ Deflate, 3:
+ Write time: 1594 ms
+ output.length: 13103224
+
+ Deflate, 4:
+ Write time: 1663 ms
+ output.length: 13380899 (!!)
+
+ 5
+ Write time: 1941 ms
+ output.length: 13171244
+
+ 6
+ Write time: 2311 ms
+ output.length: 12845101
+
+ 7: Write time: 2853 ms
+ output.length: 12759426
+
+ 8:
+ Write time: 4429 ms
+ output.length: 12624517
+
+ Deflate: DEFAULT_COMPRESSION (6?):
+ Write time: 2357 ms
+ output.length: 12845101
+
+ Deflate, BEST_COMPRESSION (9):
+ Write time: 4998 ms
+ output.length: 12600399
+ */
+
+ // TODO: Use predictor only by default for -PackBits,- LZW and ZLib/Deflate, unless explicitly disabled (ImageWriteParam)
+ int compression = TIFFImageWriteParam.getCompressionType(param);
+ OutputStream stream;
+
+ switch (compression) {
+ case TIFFBaseline.COMPRESSION_NONE:
+ return imageOutput;
+ case TIFFBaseline.COMPRESSION_PACKBITS:
+ stream = IIOUtil.createStreamAdapter(imageOutput);
+ stream = new EncoderStream(stream, new PackBitsEncoder(), true);
+ // NOTE: PackBits + Predictor is possible, but not generally supported, disable it by default
+ // (and probably not even allow it, see http://stackoverflow.com/questions/20337400/tiff-packbits-compression-with-predictor-step)
+// stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder());
+ return new DataOutputStream(stream);
+
+ case TIFFExtension.COMPRESSION_ZLIB:
+ case TIFFExtension.COMPRESSION_DEFLATE:
+ int deflateSetting = Deflater.BEST_SPEED; // This is consistent with default compression quality being 1.0 and 0 meaning max compression....
+ if (param.getCompressionMode() == ImageWriteParam.MODE_EXPLICIT) {
+ // TODO: Determine how to interpret compression quality...
+ // Docs says:
+ // A compression quality setting of 0.0 is most generically interpreted as "high compression is important,"
+ // while a setting of 1.0 is most generically interpreted as "high image quality is important."
+ // Is this what JAI TIFFImageWriter (TIFFDeflater) does? No, it does:
+ /*
+ if (param & compression etc...) {
+ float quality = param.getCompressionQuality();
+ deflateLevel = (int)(1 + 8*quality);
+ } else {
+ deflateLevel = Deflater.DEFAULT_COMPRESSION;
+ }
+ */
+ // PS: PNGImageWriter just uses hardcoded BEST_COMPRESSION... :-P
+ deflateSetting = 9 - Math.round(8 * (param.getCompressionQuality())); // This seems more correct
+ }
+
+ stream = IIOUtil.createStreamAdapter(imageOutput);
+ stream = new DeflaterOutputStream(stream, new Deflater(deflateSetting), 1024);
+ stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder());
+
+ return new DataOutputStream(stream);
+
+ case TIFFExtension.COMPRESSION_LZW:
+// stream = IIOUtil.createStreamAdapter(imageOutput);
+// stream = new EncoderStream(stream, new LZWEncoder((image.getTileWidth() * image.getTileHeight() * image.getTile(0, 0).getNumBands() * image.getColorModel().getComponentSize(0) + 7) / 8));
+// stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder());
+//
+// return new DataOutputStream(stream);
+ }
+
+ throw new IllegalArgumentException(String.format("Unsupported TIFF compression: %d", compression));
+ }
+
+ private int getPhotometricInterpretation(final ColorModel colorModel) {
+ if (colorModel.getNumComponents() == 1 && colorModel.getComponentSize(0) == 1) {
+ if (colorModel instanceof IndexColorModel) {
+ if (colorModel.getRGB(0) == 0xFFFFFF && colorModel.getRGB(1) == 0x000000) {
+ return TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO;
+ }
+ else if (colorModel.getRGB(0) != 0x000000 || colorModel.getRGB(1) != 0xFFFFFF) {
+ return TIFFBaseline.PHOTOMETRIC_PALETTE;
+ }
+ // Else, fall through to default, BLACK_IS_ZERO
+ }
+
+ return TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO;
+ }
+ else if (colorModel instanceof IndexColorModel) {
+ return TIFFBaseline.PHOTOMETRIC_PALETTE;
+ }
+
+ switch (colorModel.getColorSpace().getType()) {
+ case ColorSpace.TYPE_GRAY:
+ return TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO;
+ case ColorSpace.TYPE_RGB:
+ return TIFFBaseline.PHOTOMETRIC_RGB;
+ case ColorSpace.TYPE_CMYK:
+ return TIFFExtension.PHOTOMETRIC_SEPARATED;
+ }
+
+ throw new IllegalArgumentException("Can't determine PhotometricInterpretation for color model: " + colorModel);
+ }
+
+ private short[] createColorMap(final IndexColorModel colorModel) {
+ // TIFF6.pdf p. 23:
+ // A TIFF color map is stored as type SHORT, count = 3 * (2^BitsPerSample)
+ // "In a TIFF ColorMap, all the Red values come first, followed by the Green values, then the Blue values.
+ // In the ColorMap, black is represented by 0,0,0 and white is represented by 65535, 65535, 65535."
+ short[] colorMap = new short[(int) (3 * Math.pow(2, colorModel.getPixelSize()))];
+
+ for (int i = 0; i < colorModel.getMapSize(); i++) {
+ int color = colorModel.getRGB(i);
+ colorMap[i ] = (short) upScale((color >> 16) & 0xff);
+ colorMap[i + colorMap.length / 3] = (short) upScale((color >> 8) & 0xff);
+ colorMap[i + 2 * colorMap.length / 3] = (short) upScale((color ) & 0xff);
+ }
+
+ return colorMap;
+ }
+
+ private int upScale(final int color) {
+ return 257 * color;
+ }
+
+ private short[] asShortArray(final int[] integers) {
+ short[] shorts = new short[integers.length];
+
+ for (int i = 0; i < shorts.length; i++) {
+ shorts[i] = (short) integers[i];
+ }
+
+ return shorts;
+ }
+
+ private void writeImageData(DataOutput stream, RenderedImage renderedImage, int numComponents, int[] bandOffsets, int[] bitOffsets) throws IOException {
+ // Store 3BYTE, 4BYTE as is (possibly need to re-arrange to RGB order)
+ // Store INT_RGB as 3BYTE, INT_ARGB as 4BYTE?, INT_ABGR must be re-arranged
+ // Store IndexColorModel as is
+ // Store BYTE_GRAY as is
+ // Store USHORT_GRAY as is
+
+ processImageStarted(0);
+
+ final int minTileY = renderedImage.getMinTileY();
+ final int maxYTiles = minTileY + renderedImage.getNumYTiles();
+ final int minTileX = renderedImage.getMinTileX();
+ final int maxXTiles = minTileX + renderedImage.getNumXTiles();
+
+ // Use buffer to have longer, better performing writes
+ final int tileHeight = renderedImage.getTileHeight();
+ final int tileWidth = renderedImage.getTileWidth();
+
+ // TODO: SampleSize may differ between bands/banks
+ int sampleSize = renderedImage.getSampleModel().getSampleSize(0);
+ final ByteBuffer buffer = ByteBuffer.allocate(tileWidth * renderedImage.getSampleModel().getNumBands() * sampleSize / 8);
+
+// System.err.println("tileWidth: " + tileWidth);
+
+ for (int yTile = minTileY; yTile < maxYTiles; yTile++) {
+ for (int xTile = minTileX; xTile < maxXTiles; xTile++) {
+ final Raster tile = renderedImage.getTile(xTile, yTile);
+ final DataBuffer dataBuffer = tile.getDataBuffer();
+ final int numBands = tile.getNumBands();
+// final SampleModel sampleModel = tile.getSampleModel();
+
+ switch (dataBuffer.getDataType()) {
+ case DataBuffer.TYPE_BYTE:
+
+// System.err.println("Writing " + numBands + "BYTE -> " + numBands + "BYTE");
+ for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
+ for (int y = 0; y < tileHeight; y++) {
+ final int yOff = y * tileWidth * numBands;
+
+ for (int x = 0; x < tileWidth; x++) {
+ final int xOff = yOff + x * numBands;
+
+ for (int s = 0; s < numBands; s++) {
+ buffer.put((byte) (dataBuffer.getElem(b, xOff + bandOffsets[s]) & 0xff));
+ }
+ }
+
+ flushBuffer(buffer, stream);
+ if (stream instanceof DataOutputStream) {
+ DataOutputStream dataOutputStream = (DataOutputStream) stream;
+ dataOutputStream.flush();
+ }
+ }
+ }
+
+ break;
+
+ case DataBuffer.TYPE_USHORT:
+ case DataBuffer.TYPE_SHORT:
+ if (numComponents == 1) {
+ // TODO: This is foobar...
+// System.err.println("Writing USHORT -> " + numBands * 2 + "_BYTES");
+ for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
+ for (int y = 0; y < tileHeight; y++) {
+ final int yOff = y * tileWidth;
+
+ for (int x = 0; x < tileWidth; x++) {
+ final int xOff = yOff + x;
+
+ buffer.putShort((short) (dataBuffer.getElem(b, xOff) & 0xffff));
+ }
+
+ flushBuffer(buffer, stream);
+ if (stream instanceof DataOutputStream) {
+ DataOutputStream dataOutputStream = (DataOutputStream) stream;
+ dataOutputStream.flush();
+ }
+ }
+ }
+ }
+ else {
+// for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
+// for (int y = 0; y < tileHeight; y++) {
+// final int yOff = y * tileWidth;
+//
+// for (int x = 0; x < tileWidth; x++) {
+// final int xOff = yOff + x;
+// int element = dataBuffer.getElem(b, xOff);
+//
+// for (int s = 0; s < numBands; s++) {
+// buffer.put((byte) ((element >> bitOffsets[s]) & 0xff));
+// }
+// }
+//
+// flushBuffer(buffer, stream);
+// if (stream instanceof DataOutputStream) {
+// DataOutputStream dataOutputStream = (DataOutputStream) stream;
+// dataOutputStream.flush();
+// }
+// }
+// }
+ throw new IllegalArgumentException("Not implemented for data type: " + dataBuffer.getDataType());
+ }
+
+ break;
+
+ case DataBuffer.TYPE_INT:
+ // TODO: This is incorrect for 32 bits/sample, only works for packed (INT_(A)RGB)
+// System.err.println("Writing INT -> " + numBands + "_BYTES");
+ for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
+ for (int y = 0; y < tileHeight; y++) {
+ final int yOff = y * tileWidth;
+
+ for (int x = 0; x < tileWidth; x++) {
+ final int xOff = yOff + x;
+ int element = dataBuffer.getElem(b, xOff);
+
+ for (int s = 0; s < numBands; s++) {
+ buffer.put((byte) ((element >> bitOffsets[s]) & 0xff));
+ }
+ }
+
+ flushBuffer(buffer, stream);
+ if (stream instanceof DataOutputStream) {
+ DataOutputStream dataOutputStream = (DataOutputStream) stream;
+ dataOutputStream.flush();
+ }
+ }
+ }
+
+ break;
+ default:
+ throw new IllegalArgumentException("Not implemented for data type: " + dataBuffer.getDataType());
+ }
+ }
+
+ // TODO: Need to flush/start new compression for each row, for proper LZW/PackBits/Deflate/ZLib
+ if (stream instanceof DataOutputStream) {
+ DataOutputStream dataOutputStream = (DataOutputStream) stream;
+ dataOutputStream.flush();
+ }
+
+ // TODO: Report better progress
+ processImageProgress((100f * yTile) / maxYTiles);
+ }
+
+ if (stream instanceof DataOutputStream) {
+ DataOutputStream dataOutputStream = (DataOutputStream) stream;
+ dataOutputStream.close();
+ }
+
+ processImageComplete();
+ }
+
+ // TODO: Would be better to solve this on stream level... But writers would then have to explicitly flush the buffer before done.
+ private void flushBuffer(final ByteBuffer buffer, final DataOutput stream) throws IOException {
+ buffer.flip();
+ stream.write(buffer.array(), buffer.arrayOffset(), buffer.remaining());
+
+ buffer.clear();
+ }
+
+ // Metadata
+
+ @Override
+ public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) {
+ return null;
+ }
+
+ @Override
+ public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) {
+ return null;
+ }
+
+ // Param
+
+ @Override
+ public ImageWriteParam getDefaultWriteParam() {
+ return new TIFFImageWriteParam();
+ }
+
+ // Test
+
+ public static void main(String[] args) throws IOException {
+ int argIdx = 0;
+
+ // TODO: Proper argument parsing: -t -c
+ int type = args.length > argIdx + 1 ? Integer.parseInt(args[argIdx++]) : -1;
+ int compression = args.length > argIdx + 1 ? Integer.parseInt(args[argIdx++]) : 0;
+
+ if (args.length <= argIdx) {
+ System.err.println("No file specified");
+ System.exit(1);
+ }
+
+ File file = new File(args[argIdx++]);
+
+ BufferedImage original;
+// BufferedImage original = ImageIO.read(file);
+ ImageInputStream inputStream = ImageIO.createImageInputStream(file);
+ try {
+ Iterator readers = ImageIO.getImageReaders(inputStream);
+
+ if (!readers.hasNext()) {
+ System.err.println("No reader for: " + file);
+ System.exit(1);
+ }
+
+ ImageReader reader = readers.next();
+ reader.setInput(inputStream);
+
+ ImageReadParam param = reader.getDefaultReadParam();
+ param.setDestinationType(reader.getRawImageType(0));
+
+ if (param.getDestinationType() == null) {
+ Iterator types = reader.getImageTypes(0);
+
+ while (types.hasNext()) {
+ ImageTypeSpecifier typeSpecifier = types.next();
+
+ if (typeSpecifier.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_CMYK) {
+ param.setDestinationType(typeSpecifier);
+ }
+ }
+ }
+
+ System.err.println("param.getDestinationType(): " + param.getDestinationType());
+
+ original = reader.read(0, param);
+ }
+ finally {
+ inputStream.close();
+ }
+
+ System.err.println("original: " + original);
+
+// BufferedImage image = original;
+// BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
+// BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_RGB);
+// BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
+// BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_BGR);
+// BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
+ BufferedImage image;
+ if (type < 0 || type == original.getType()) {
+ image = original;
+ }
+ else if (type == BufferedImage.TYPE_BYTE_INDEXED) {
+// image = ImageUtil.createIndexed(original, 256, null, ImageUtil.COLOR_SELECTION_QUALITY | ImageUtil.DITHER_DIFFUSION_ALTSCANS);
+ image = ImageUtil.createIndexed(original, 256, null, ImageUtil.COLOR_SELECTION_FAST | ImageUtil.DITHER_DIFFUSION_ALTSCANS);
+ }
+ else {
+ image = new BufferedImage(original.getWidth(), original.getHeight(), type);
+ Graphics2D graphics = image.createGraphics();
+
+ try {
+ graphics.drawImage(original, 0, 0, null);
+ }
+ finally {
+ graphics.dispose();
+ }
+ }
+
+ original = null;
+
+ File output = File.createTempFile(file.getName().replace('.', '-'), ".tif");
+// output.deleteOnExit();
+
+ System.err.println("output: " + output);
+ TIFFImageWriter writer = new TIFFImageWriter(null);
+// ImageWriter writer = ImageIO.getImageWritersByFormatName("PNG").next();
+// ImageWriter writer = ImageIO.getImageWritersByFormatName("BMP").next();
+ ImageOutputStream stream = ImageIO.createImageOutputStream(output);
+
+ try {
+ writer.setOutput(stream);
+
+ ImageWriteParam param = writer.getDefaultWriteParam();
+ param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
+// param.setCompressionType("None");
+// param.setCompressionType("PackBits");
+// param.setCompressionType("ZLib");
+ param.setCompressionType(param.getCompressionTypes()[compression]);
+// if (compression == 2) {
+// param.setCompressionQuality(0);
+// }
+ System.err.println("compression: " + param.getLocalizedCompressionTypeName());
+
+ long start = System.currentTimeMillis();
+ writer.write(null, new IIOImage(image, null, null), param);
+ System.err.println("Write time: " + (System.currentTimeMillis() - start) + " ms");
+ }
+ finally {
+ stream.close();
+ }
+
+ System.err.println("output.length: " + output.length());
+
+ // TODO: Support writing multipage TIFF
+// ImageOutputStream stream = ImageIO.createImageOutputStream(output);
+// try {
+// writer.setOutput(stream);
+// writer.prepareWriteSequence(null);
+// for(int i = 0; i < images.size(); i ++){
+// writer.writeToSequence(new IIOImage(images.get(i), null, null), null);
+// }
+// writer.endWriteSequence();
+// }
+// finally {
+// stream.close();
+// }
+// writer.dispose();
+
+
+ image = null;
+
+ BufferedImage read = ImageIO.read(output);
+ System.err.println("read: " + read);
+
+ TIFFImageReader.showIt(read, output.getName());
+ }
+}
diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterSpi.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterSpi.java
new file mode 100644
index 00000000..5bb96699
--- /dev/null
+++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterSpi.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2014, 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.imageio.plugins.tiff;
+
+import com.twelvemonkeys.imageio.spi.ProviderInfo;
+import com.twelvemonkeys.imageio.util.IIOUtil;
+
+import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.ImageWriter;
+import javax.imageio.spi.ImageWriterSpi;
+import javax.imageio.stream.ImageOutputStream;
+import java.io.IOException;
+import java.util.Locale;
+
+/**
+ * TIFFImageWriterSpi
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: TIFFImageWriterSpi.java,v 1.0 18.09.13 12:46 haraldk Exp$
+ */
+public final class TIFFImageWriterSpi extends ImageWriterSpi {
+ // TODO: Implement canEncodeImage better
+
+ public TIFFImageWriterSpi() {
+ this(IIOUtil.getProviderInfo(TIFFImageWriterSpi.class));
+ }
+
+ private TIFFImageWriterSpi(final ProviderInfo providerInfo) {
+ super(
+ providerInfo.getVendorName(), providerInfo.getVersion(),
+ new String[] {"tiff", "TIFF", "tif", "TIFF"},
+ new String[] {"tif", "tiff"},
+ new String[] {"image/tiff", "image/x-tiff"},
+ "com.twelvemonkeys.imageio.plugins.tiff.TIFFImageWriter",
+ new Class>[] {ImageOutputStream.class},
+ new String[] {"com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReaderSpi"},
+ true, // supports standard stream metadata
+ null, null, // native stream format name and class
+ null, null, // extra stream formats
+ true, // supports standard image metadata
+ null, null,
+ null, null // extra image metadata formats
+ );
+ }
+
+ @Override
+ public boolean canEncodeImage(ImageTypeSpecifier type) {
+ // TODO: Test bit depths compatibility
+
+ return true;
+ }
+
+ @Override
+ public ImageWriter createWriterInstance(Object extension) throws IOException {
+ return new TIFFImageWriter(this);
+ }
+
+ @Override
+ public String getDescription(Locale locale) {
+ return "Aldus/Adobe Tagged Image File Format (TIFF) image writer";
+ }
+}
diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDifferencingStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDifferencingStreamTest.java
new file mode 100644
index 00000000..30fc6608
--- /dev/null
+++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDifferencingStreamTest.java
@@ -0,0 +1,574 @@
+/*
+ * Copyright (c) 2014, 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.imageio.plugins.tiff;
+
+import com.twelvemonkeys.io.FastByteArrayOutputStream;
+import com.twelvemonkeys.io.LittleEndianDataInputStream;
+import com.twelvemonkeys.io.LittleEndianDataOutputStream;
+import org.junit.Test;
+
+import java.io.*;
+import java.nio.ByteOrder;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * HorizontalDifferencingStreamTest
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: HorizontalDifferencingStreamTest.java,v 1.0 02.12.13 09:50 haraldk Exp$
+ */
+public class HorizontalDifferencingStreamTest {
+
+ @Test
+ public void testWrite1SPP1BPS() throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ OutputStream stream = new HorizontalDifferencingStream(bytes, 24, 1, 1, ByteOrder.BIG_ENDIAN);
+
+ // Row 1
+ stream.write(0xff);
+ stream.write(0xff);
+ stream.write(0xff);
+
+ // Row 2
+ stream.write(0x5e);
+ stream.write(0x1e);
+ stream.write(0x78);
+
+
+ // 1 sample per pixel, 1 bits per sample (mono/indexed)
+ byte[] data = {
+ (byte) 0x80, 0x00, 0x00,
+ 0x71, 0x11, 0x44,
+ };
+
+ assertArrayEquals(data, bytes.toByteArray());
+ }
+
+ @Test
+ public void testWrite1SPP2BPS() throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ OutputStream stream = new HorizontalDifferencingStream(bytes, 16, 1, 2, ByteOrder.BIG_ENDIAN);
+
+ // Row 1
+ stream.write(0xff);
+ stream.write(0xff);
+ stream.write(0xff);
+ stream.write(0xff);
+
+ // Row 2
+ stream.write(0x41);
+ stream.write(0x6b);
+ stream.write(0x05);
+ stream.write(0x0f);
+
+ // 1 sample per pixel, 2 bits per sample (gray/indexed)
+ byte[] data = {
+ (byte) 0xc0, 0x00, 0x00, 0x00,
+ 0x71, 0x11, 0x44, (byte) 0xcc,
+ };
+
+ assertArrayEquals(data, bytes.toByteArray());
+ }
+
+ @Test
+ public void testWrite1SPP4BPS() throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ OutputStream stream = new HorizontalDifferencingStream(bytes, 8, 1, 4, ByteOrder.BIG_ENDIAN);
+
+ // Row 1
+ stream.write(0xff);
+ stream.write(0xff);
+ stream.write(0xff);
+ stream.write(0xff);
+
+ // Row 2
+ stream.write(0x77);
+ stream.write(0x89);
+ stream.write(0xd1);
+ stream.write(0xd9);
+
+ // Row 3
+ stream.write(0x00);
+ stream.write(0x01);
+ stream.write(0x22);
+ stream.write(0x00);
+
+ // 1 sample per pixel, 4 bits per sample (gray/indexed)
+ byte[] data = {
+ (byte) 0xf0, 0x00, 0x00, 0x00,
+ 0x70, 0x11, 0x44, (byte) 0xcc,
+ 0x00, 0x01, 0x10, (byte) 0xe0
+ };
+
+ assertArrayEquals(data, bytes.toByteArray());
+ }
+
+ @Test
+ public void testWrite1SPP8BPS() throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ OutputStream stream = new HorizontalDifferencingStream(bytes, 4, 1, 8, ByteOrder.BIG_ENDIAN);
+
+ // Row 1
+ stream.write(0xff);
+ stream.write(0xff);
+ stream.write(0xff);
+ stream.write(0xff);
+
+ // Row 2
+ stream.write(0x7f);
+ stream.write(0x80);
+ stream.write(0x84);
+ stream.write(0x80);
+
+ // Row 3
+ stream.write(0x00);
+ stream.write(0x7f);
+ stream.write(0xfe);
+ stream.write(0x7f);
+
+ // 1 sample per pixel, 8 bits per sample (gray/indexed)
+ byte[] data = {
+ (byte) 0xff, 0, 0, 0,
+ 0x7f, 1, 4, -4,
+ 0x00, 127, 127, -127
+ };
+
+ assertArrayEquals(data, bytes.toByteArray());
+ }
+
+ @Test
+ public void testWriteArray1SPP8BPS() throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ OutputStream stream = new HorizontalDifferencingStream(bytes, 4, 1, 8, ByteOrder.BIG_ENDIAN);
+
+ stream.write(new byte[] {
+ (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+ 0x7f, (byte) 0x80, (byte) 0x84, (byte) 0x80,
+ 0x00, 0x7f, (byte) 0xfe, 0x7f,
+ });
+
+ // 1 sample per pixel, 8 bits per sample (gray/indexed)
+ byte[] data = {
+ (byte) 0xff, 0, 0, 0,
+ 0x7f, 1, 4, -4,
+ 0x00, 127, 127, -127
+ };
+
+ assertArrayEquals(data, bytes.toByteArray());
+ }
+
+ @Test
+ public void testWrite1SPP32BPS() throws IOException {
+ // 1 sample per pixel, 32 bits per sample (gray)
+ FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(16);
+ OutputStream out = new HorizontalDifferencingStream(bytes, 4, 1, 32, ByteOrder.BIG_ENDIAN);
+ DataOutput dataOut = new DataOutputStream(out);
+ dataOut.writeInt(0x00000000);
+ dataOut.writeInt(305419896);
+ dataOut.writeInt(305419896);
+ dataOut.writeInt(-610839792);
+
+ InputStream in = bytes.createInputStream();
+ DataInput dataIn = new DataInputStream(in);
+
+ // Row 1
+ assertEquals(0, dataIn.readInt());
+ assertEquals(305419896, dataIn.readInt());
+ assertEquals(0, dataIn.readInt());
+ assertEquals(-916259688, dataIn.readInt());
+
+ // EOF
+ assertEquals(-1, in.read());
+ }
+
+ @Test
+ public void testWrite1SPP32BPSLittleEndian() throws IOException {
+ // 1 sample per pixel, 32 bits per sample (gray)
+ FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(16);
+ OutputStream out = new HorizontalDifferencingStream(bytes, 4, 1, 32, ByteOrder.LITTLE_ENDIAN);
+ DataOutput dataOut = new LittleEndianDataOutputStream(out);
+ dataOut.writeInt(0x00000000);
+ dataOut.writeInt(305419896);
+ dataOut.writeInt(305419896);
+ dataOut.writeInt(-610839792);
+
+ InputStream in = bytes.createInputStream();
+ DataInput dataIn = new LittleEndianDataInputStream(in);
+
+ // Row 1
+ assertEquals(0, dataIn.readInt());
+ assertEquals(305419896, dataIn.readInt());
+ assertEquals(0, dataIn.readInt());
+ assertEquals(-916259688, dataIn.readInt());
+
+ // EOF
+ assertEquals(-1, in.read());
+ }
+
+ @Test
+ public void testWrite1SPP64BPS() throws IOException {
+ // 1 sample per pixel, 64 bits per sample (gray)
+ FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(32);
+
+ OutputStream out = new HorizontalDifferencingStream(bytes, 4, 1, 64, ByteOrder.BIG_ENDIAN);
+ DataOutput dataOut = new DataOutputStream(out);
+ dataOut.writeLong(0x00000000);
+ dataOut.writeLong(81985529216486895L);
+ dataOut.writeLong(81985529216486895L);
+ dataOut.writeLong(-163971058432973790L);
+
+ InputStream in = bytes.createInputStream();
+ DataInput dataIn = new DataInputStream(in);
+
+ // Row 1
+ assertEquals(0, dataIn.readLong());
+ assertEquals(81985529216486895L, dataIn.readLong());
+ assertEquals(0, dataIn.readLong());
+ assertEquals(-245956587649460685L, dataIn.readLong());
+
+ // EOF
+ assertEquals(-1, in.read());
+ }
+
+ @Test
+ public void testWrite1SPP64BPSLittleEndian() throws IOException {
+ // 1 sample per pixel, 64 bits per sample (gray)
+ FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(32);
+
+ OutputStream out = new HorizontalDifferencingStream(bytes, 4, 1, 64, ByteOrder.LITTLE_ENDIAN);
+ DataOutput dataOut = new LittleEndianDataOutputStream(out);
+ dataOut.writeLong(0x00000000);
+ dataOut.writeLong(81985529216486895L);
+ dataOut.writeLong(81985529216486895L);
+ dataOut.writeLong(-163971058432973790L);
+
+ InputStream in = bytes.createInputStream();
+ DataInput dataIn = new LittleEndianDataInputStream(in);
+
+ // Row 1
+ assertEquals(0, dataIn.readLong());
+ assertEquals(81985529216486895L, dataIn.readLong());
+ assertEquals(0, dataIn.readLong());
+ assertEquals(-245956587649460685L, dataIn.readLong());
+
+ // EOF
+ assertEquals(-1, in.read());
+ }
+
+ @Test
+ public void testWrite3SPP8BPS() throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ OutputStream stream = new HorizontalDifferencingStream(bytes, 4, 3, 8, ByteOrder.BIG_ENDIAN);
+
+ // Row 1
+ stream.write(0xff);
+ stream.write(0x00);
+ stream.write(0x7f);
+
+ stream.write(0xfe);
+ stream.write(0xff);
+ stream.write(0x7e);
+
+ stream.write(0xfa);
+ stream.write(0xfb);
+ stream.write(0x7a);
+
+ stream.write(0xfe);
+ stream.write(0xff);
+ stream.write(0x7e);
+
+ // Row 2
+ stream.write(0x7f);
+ stream.write(0x7f);
+ stream.write(0x7f);
+
+ stream.write(0x80);
+ stream.write(0x80);
+ stream.write(0x80);
+
+ stream.write(0x84);
+ stream.write(0x84);
+ stream.write(0x84);
+
+ stream.write(0x80);
+ stream.write(0x80);
+ stream.write(0x80);
+
+ // Row 3
+ stream.write(0x00);
+ stream.write(0x00);
+ stream.write(0x00);
+
+ stream.write(0x7f);
+ stream.write(0x81);
+ stream.write(0x00);
+
+ stream.write(0x00);
+ stream.write(0x00);
+ stream.write(0x00);
+
+ stream.write(0x00);
+ stream.write(0x00);
+ stream.write(0x7f);
+
+ // 3 samples per pixel, 8 bits per sample (RGB)
+ byte[] data = {
+ (byte) 0xff, (byte) 0x00, (byte) 0x7f, -1, -1, -1, -4, -4, -4, 4, 4, 4,
+ 0x7f, 0x7f, 0x7f, 1, 1, 1, 4, 4, 4, -4, -4, -4,
+ 0x00, 0x00, 0x00, 127, -127, 0, -127, 127, 0, 0, 0, 127,
+ };
+
+ assertArrayEquals(data, bytes.toByteArray());
+
+ }
+
+ @Test
+ public void testWrite3SPP16BPS() throws IOException {
+ FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(24);
+ OutputStream out = new HorizontalDifferencingStream(bytes, 4, 3, 16, ByteOrder.BIG_ENDIAN);
+
+ DataOutput dataOut = new DataOutputStream(out);
+ dataOut.writeShort(0x0000);
+ dataOut.writeShort(0x0000);
+ dataOut.writeShort(0x0000);
+ dataOut.writeShort(4660);
+ dataOut.writeShort(30292);
+ dataOut.writeShort(4660);
+ dataOut.writeShort(4660);
+ dataOut.writeShort(30292);
+ dataOut.writeShort(4660);
+ dataOut.writeShort(-9320);
+ dataOut.writeShort(-60584);
+ dataOut.writeShort(-9320);
+
+ dataOut.writeShort(0x0000);
+ dataOut.writeShort(0x0000);
+ dataOut.writeShort(0x0000);
+ dataOut.writeShort(30292);
+ dataOut.writeShort(30292);
+ dataOut.writeShort(30292);
+ dataOut.writeShort(30292);
+ dataOut.writeShort(30292);
+ dataOut.writeShort(30292);
+ dataOut.writeShort(-60584);
+ dataOut.writeShort(-60584);
+ dataOut.writeShort(-60584);
+
+ InputStream in = bytes.createInputStream();
+ DataInput dataIn = new DataInputStream(in);
+
+ // Row 1
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(4660, dataIn.readUnsignedShort());
+ assertEquals(30292, dataIn.readUnsignedShort());
+ assertEquals(4660, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(51556, dataIn.readUnsignedShort());
+ assertEquals(40196, dataIn.readUnsignedShort());
+ assertEquals(51556, dataIn.readUnsignedShort());
+
+ // Row 2
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(30292, dataIn.readUnsignedShort());
+ assertEquals(30292, dataIn.readUnsignedShort());
+ assertEquals(30292, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(40196, dataIn.readUnsignedShort());
+ assertEquals(40196, dataIn.readUnsignedShort());
+ assertEquals(40196, dataIn.readUnsignedShort());
+
+ // EOF
+ assertEquals(-1, in.read());
+ }
+
+ @Test
+ public void testWrite3SPP16BPSLittleEndian() throws IOException {
+ FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(24);
+
+ OutputStream out = new HorizontalDifferencingStream(bytes, 4, 3, 16, ByteOrder.LITTLE_ENDIAN);
+ DataOutput dataOut = new LittleEndianDataOutputStream(out);
+ dataOut.writeShort(0x0000);
+ dataOut.writeShort(0x0000);
+ dataOut.writeShort(0x0000);
+ dataOut.writeShort(4660);
+ dataOut.writeShort(30292);
+ dataOut.writeShort(4660);
+ dataOut.writeShort(4660);
+ dataOut.writeShort(30292);
+ dataOut.writeShort(4660);
+ dataOut.writeShort(-9320);
+ dataOut.writeShort(-60584);
+ dataOut.writeShort(-9320);
+
+ dataOut.writeShort(0x0000);
+ dataOut.writeShort(0x0000);
+ dataOut.writeShort(0x0000);
+ dataOut.writeShort(30292);
+ dataOut.writeShort(30292);
+ dataOut.writeShort(30292);
+ dataOut.writeShort(30292);
+ dataOut.writeShort(30292);
+ dataOut.writeShort(30292);
+ dataOut.writeShort(-60584);
+ dataOut.writeShort(-60584);
+ dataOut.writeShort(-60584);
+
+ InputStream in = bytes.createInputStream();
+ DataInput dataIn = new LittleEndianDataInputStream(in);
+
+ // Row 1
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(4660, dataIn.readUnsignedShort());
+ assertEquals(30292, dataIn.readUnsignedShort());
+ assertEquals(4660, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(51556, dataIn.readUnsignedShort());
+ assertEquals(40196, dataIn.readUnsignedShort());
+ assertEquals(51556, dataIn.readUnsignedShort());
+
+ // Row 2
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(30292, dataIn.readUnsignedShort());
+ assertEquals(30292, dataIn.readUnsignedShort());
+ assertEquals(30292, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(0, dataIn.readUnsignedShort());
+ assertEquals(40196, dataIn.readUnsignedShort());
+ assertEquals(40196, dataIn.readUnsignedShort());
+ assertEquals(40196, dataIn.readUnsignedShort());
+
+ // EOF
+ assertEquals(-1, in.read());
+ }
+
+ @Test
+ public void testWrite4SPP8BPS() throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ OutputStream stream = new HorizontalDifferencingStream(bytes, 4, 4, 8, ByteOrder.BIG_ENDIAN);
+
+ // Row 1
+ stream.write(0xff);
+ stream.write(0x00);
+ stream.write(0x7f);
+ stream.write(0x00);
+
+ stream.write(0xfe);
+ stream.write(0xff);
+ stream.write(0x7e);
+ stream.write(0xff);
+
+ stream.write(0xfa);
+ stream.write(0xfb);
+ stream.write(0x7a);
+ stream.write(0xfb);
+
+ stream.write(0xfe);
+ stream.write(0xff);
+ stream.write(0x7e);
+ stream.write(0xff);
+
+ // Row 2
+ stream.write(0x7f);
+ stream.write(0x7f);
+ stream.write(0x7f);
+ stream.write(0x7f);
+
+ stream.write(0x80);
+ stream.write(0x80);
+ stream.write(0x80);
+ stream.write(0x80);
+
+ stream.write(0x84);
+ stream.write(0x84);
+ stream.write(0x84);
+ stream.write(0x84);
+
+ stream.write(0x80);
+ stream.write(0x80);
+ stream.write(0x80);
+ stream.write(0x80);
+
+ // 4 samples per pixel, 8 bits per sample (RGBA)
+ byte[] data = {
+ (byte) 0xff, (byte) 0x00, (byte) 0x7f, 0x00, -1, -1, -1, -1, -4, -4, -4, -4, 4, 4, 4, 4,
+ 0x7f, 0x7f, 0x7f, 0x7f, 1, 1, 1, 1, 4, 4, 4, 4, -4, -4, -4, -4,
+ };
+
+ assertArrayEquals(data, bytes.toByteArray());
+ }
+
+ @Test
+ public void testWriteArray4SPP8BPS() throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ OutputStream stream = new HorizontalDifferencingStream(bytes, 4, 4, 8, ByteOrder.BIG_ENDIAN);
+
+ stream.write(
+ new byte[] {
+ (byte) 0xff, 0x00, 0x7f, 0x00,
+ (byte) 0xfe, (byte) 0xff, 0x7e, (byte) 0xff,
+ (byte) 0xfa, (byte) 0xfb, 0x7a, (byte) 0xfb,
+ (byte) 0xfe, (byte) 0xff, 0x7e, (byte) 0xff,
+
+ 0x7f, 0x7f, 0x7f, 0x7f,
+ (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
+ (byte) 0x84, (byte) 0x84, (byte) 0x84, (byte) 0x84,
+ (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
+ }
+ );
+
+ // 4 samples per pixel, 8 bits per sample (RGBA)
+ byte[] data = {
+ (byte) 0xff, (byte) 0x00, (byte) 0x7f, 0x00, -1, -1, -1, -1, -4, -4, -4, -4, 4, 4, 4, 4,
+ 0x7f, 0x7f, 0x7f, 0x7f, 1, 1, 1, 1, 4, 4, 4, 4, -4, -4, -4, -4,
+ };
+
+ assertArrayEquals(data, bytes.toByteArray());
+ }
+
+
+}
diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java
new file mode 100644
index 00000000..8c42c5a9
--- /dev/null
+++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2014, 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.imageio.plugins.tiff;
+
+import com.twelvemonkeys.imageio.util.ImageWriterAbstractTestCase;
+
+import javax.imageio.ImageWriter;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.RenderedImage;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * TIFFImageWriterTest
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: TIFFImageWriterTest.java,v 1.0 19.09.13 13:22 haraldk Exp$
+ */
+public class TIFFImageWriterTest extends ImageWriterAbstractTestCase {
+
+ public static final TIFFImageWriterSpi PROVIDER = new TIFFImageWriterSpi();
+
+ @Override
+ protected ImageWriter createImageWriter() {
+ return new TIFFImageWriter(PROVIDER);
+ }
+
+ @Override
+ protected List extends RenderedImage> getTestData() {
+ BufferedImage image = new BufferedImage(300, 200, BufferedImage.TYPE_INT_ARGB);
+ Graphics2D graphics = image.createGraphics();
+ try {
+ graphics.setColor(Color.RED);
+ graphics.fillRect(0, 0, 100, 200);
+ graphics.setColor(Color.BLUE);
+ graphics.fillRect(100, 0, 100, 200);
+ graphics.clearRect(200, 0, 100, 200);
+ }
+ finally {
+ graphics.dispose();
+ }
+
+ return Arrays.asList(image);
+ }
+}