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");
}
}
}