From 1505aa651bc672503675acf88ae2826b4782f8af Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 18 Mar 2015 21:46:04 +0100 Subject: [PATCH] TMI-26: TIFF write support sans LZW. --- .../imageio/metadata/AbstractEntry.java | 49 +- .../imageio/metadata/exif/EXIFReader.java | 2 + .../imageio/metadata/exif/EXIFWriter.java | 412 ++++++++++ .../metadata/MetadataReaderAbstractTest.java | 45 +- .../imageio/metadata/exif/EXIFWriterTest.java | 312 ++++++++ .../tiff/HorizontalDifferencingStream.java | 302 +++++++ .../imageio/plugins/tiff/LZWDecoder.java | 4 +- .../plugins/tiff/TIFFImageReaderSpi.java | 9 +- .../plugins/tiff/TIFFImageWriteParam.java | 108 +++ .../imageio/plugins/tiff/TIFFImageWriter.java | 734 ++++++++++++++++++ .../plugins/tiff/TIFFImageWriterSpi.java | 89 +++ .../HorizontalDifferencingStreamTest.java | 574 ++++++++++++++ .../plugins/tiff/TIFFImageWriterTest.java | 73 ++ 13 files changed, 2663 insertions(+), 50 deletions(-) create mode 100644 imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriter.java create mode 100644 imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriterTest.java create mode 100644 imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDifferencingStream.java create mode 100644 imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriteParam.java create mode 100644 imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java create mode 100644 imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterSpi.java create mode 100644 imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDifferencingStreamTest.java create mode 100644 imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java 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 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); + } +}