TMI-26: TIFF write support sans LZW.

This commit is contained in:
Harald Kuhr 2015-03-18 21:46:04 +01:00
parent 824613b4f1
commit 1505aa651b
13 changed files with 2663 additions and 50 deletions

View File

@ -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();

View File

@ -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();

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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<Entry> 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<Entry> 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
// <data including sub-IFDs>
// 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<Entry> 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<Entry> entries = new ArrayList<Entry>(directory.size());
for (Entry entry : directory) {
entries.add(entry);
}
Collections.sort(entries, new Comparator<Entry>() {
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;
}
}

View File

@ -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) {

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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<Entry> entries = new ArrayList<Entry>();
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<Entry> entries = new ArrayList<Entry>();
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<Entry> entries = new ArrayList<Entry>();
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.<Entry>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<Entry> entries = new ArrayList<Entry>();
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<Entry> entries = Collections.<Entry>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");
}
}
}

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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();
}
}
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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()));
}
}

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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<Entry> entries = new ArrayList<Entry>();
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 <type> -c <compression>
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<ImageReader> 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<ImageTypeSpecifier> 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());
}
}

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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";
}
}

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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());
}
}

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: TIFFImageWriterTest.java,v 1.0 19.09.13 13:22 haraldk Exp$
*/
public class TIFFImageWriterTest extends ImageWriterAbstractTestCase {
public static final TIFFImageWriterSpi PROVIDER = new TIFFImageWriterSpi();
@Override
protected ImageWriter createImageWriter() {
return new TIFFImageWriter(PROVIDER);
}
@Override
protected List<? extends RenderedImage> getTestData() {
BufferedImage image = new BufferedImage(300, 200, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = image.createGraphics();
try {
graphics.setColor(Color.RED);
graphics.fillRect(0, 0, 100, 200);
graphics.setColor(Color.BLUE);
graphics.fillRect(100, 0, 100, 200);
graphics.clearRect(200, 0, 100, 200);
}
finally {
graphics.dispose();
}
return Arrays.asList(image);
}
}