diff --git a/contrib/src/main/java/com/twelvemonkeys/contrib/tiff/TIFFUtilities.java b/contrib/src/main/java/com/twelvemonkeys/contrib/tiff/TIFFUtilities.java index 8835c038..9511cadd 100644 --- a/contrib/src/main/java/com/twelvemonkeys/contrib/tiff/TIFFUtilities.java +++ b/contrib/src/main/java/com/twelvemonkeys/contrib/tiff/TIFFUtilities.java @@ -29,11 +29,14 @@ package com.twelvemonkeys.contrib.tiff; import com.twelvemonkeys.image.AffineTransformOp; -import com.twelvemonkeys.imageio.metadata.*; -import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; -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.metadata.AbstractDirectory; +import com.twelvemonkeys.imageio.metadata.CompoundDirectory; +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.tiff.TIFF; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFWriter; import com.twelvemonkeys.lang.Validate; import javax.imageio.IIOException; @@ -44,7 +47,6 @@ import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; -import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -57,7 +59,7 @@ import java.util.List; * @author last modified by $Author$ * @version $Id$ */ -public class TIFFUtilities { +public final class TIFFUtilities { private TIFFUtilities() { } @@ -201,7 +203,7 @@ public class TIFFUtilities { public static List getPages(ImageInputStream imageInput) throws IOException { ArrayList pages = new ArrayList(); - CompoundDirectory IFDs = (CompoundDirectory) new EXIFReader().read(imageInput); + CompoundDirectory IFDs = (CompoundDirectory) new TIFFReader().read(imageInput); int pageCount = IFDs.directoryCount(); for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) { @@ -212,7 +214,7 @@ public class TIFFUtilities { } public static void writePages(ImageOutputStream imageOutput, List pages) throws IOException { - EXIFWriter exif = new EXIFWriter(); + TIFFWriter exif = new TIFFWriter(); long nextPagePos = imageOutput.getStreamPosition(); if (nextPagePos == 0) { exif.writeTIFFHeader(imageOutput); @@ -316,9 +318,9 @@ public class TIFFUtilities { this.stream = stream; } - private long write(ImageOutputStream outputStream, EXIFWriter exifWriter) throws IOException { + private long write(ImageOutputStream outputStream, TIFFWriter tiffWriter) throws IOException { List newIFD = writeDirectoryData(IFD, outputStream); - return exifWriter.writeIFD(newIFD, outputStream); + return tiffWriter.writeIFD(newIFD, outputStream); } private List writeDirectoryData(Directory IFD, ImageOutputStream outputStream) throws IOException { @@ -506,78 +508,9 @@ public class TIFFUtilities { break; } } + newIDFData.add(new TIFFEntry(TIFF.TAG_ORIENTATION, (short) orientation)); - IFD = new AbstractDirectory(newIDFData) { - }; - } - } - - /** - * TODO: Temporary clone, to be removed after TMI204 has been closed - */ - public static final class TIFFEntry extends AbstractEntry { - // TODO: Expose a merge of this and the EXIFEntry class... - private final short type; - - private static short guessType(final Object val) { - // TODO: This code is duplicated in EXIFWriter.getType, needs refactor! - Object value = Validate.notNull(val); - - 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 guessType not implemented for value of type %s", value.getClass())); - } - - public TIFFEntry(final int identifier, final Object value) { - this(identifier, guessType(value), value); - } - - TIFFEntry(int identifier, short type, Object value) { - super(identifier, value); - this.type = type; - } - - @Override - public String getTypeName() { - return TIFF.TYPE_NAMES[type]; + IFD = new AbstractDirectory(newIDFData) {}; } } diff --git a/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java index 20aa85cf..335deba2 100644 --- a/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java +++ b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java @@ -31,13 +31,13 @@ package com.twelvemonkeys.imageio.path; import com.twelvemonkeys.imageio.metadata.CompoundDirectory; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; -import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; -import com.twelvemonkeys.imageio.metadata.exif.TIFF; import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment; import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; import com.twelvemonkeys.imageio.metadata.psd.PSD; import com.twelvemonkeys.imageio.metadata.psd.PSDReader; +import com.twelvemonkeys.imageio.metadata.tiff.TIFF; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.stream.SubImageInputStream; @@ -129,7 +129,7 @@ public final class Paths { else if (magic >>> 16 == TIFF.BYTE_ORDER_MARK_BIG_ENDIAN && (magic & 0xffff) == TIFF.TIFF_MAGIC || magic >>> 16 == TIFF.BYTE_ORDER_MARK_LITTLE_ENDIAN && (magic & 0xffff) == TIFF.TIFF_MAGIC << 8) { // TIFF version - CompoundDirectory IFDs = (CompoundDirectory) new EXIFReader().read(stream); + CompoundDirectory IFDs = (CompoundDirectory) new TIFFReader().read(stream); Directory directory = IFDs.getDirectory(0); Entry photoshop = directory.getEntryById(TIFF.TAG_PHOTOSHOP); diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java index fe249093..cd70f04c 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java @@ -31,7 +31,7 @@ package com.twelvemonkeys.imageio.plugins.jpeg; import com.twelvemonkeys.imageio.color.YCbCrConverter; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; -import com.twelvemonkeys.imageio.metadata.exif.TIFF; +import com.twelvemonkeys.imageio.metadata.tiff.TIFF; import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.lang.Validate; diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index a6131d9a..ec522ca9 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -34,11 +34,11 @@ import com.twelvemonkeys.imageio.color.YCbCrConverter; import com.twelvemonkeys.imageio.metadata.CompoundDirectory; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; -import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; -import com.twelvemonkeys.imageio.metadata.exif.TIFF; import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment; import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; +import com.twelvemonkeys.imageio.metadata.tiff.TIFF; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; import com.twelvemonkeys.imageio.util.ProgressListenerBase; import com.twelvemonkeys.lang.Validate; @@ -790,9 +790,9 @@ public final class JPEGImageReader extends ImageReaderBase { } else { ImageInputStream stream = ImageIO.createImageInputStream(data); - return (CompoundDirectory) new EXIFReader().read(stream); + return (CompoundDirectory) new TIFFReader().read(stream); - // TODO: Directory offset of thumbnail is wrong/relative to container stream, causing trouble for the EXIFReader... + // TODO: Directory offset of thumbnail is wrong/relative to container stream, causing trouble for the TIFFReader... } } @@ -981,7 +981,7 @@ public final class JPEGImageReader extends ImageReaderBase { } else { ImageInputStream stream = new MemoryCacheImageInputStream(data); - CompoundDirectory exifMetadata = (CompoundDirectory) new EXIFReader().read(stream); + CompoundDirectory exifMetadata = (CompoundDirectory) new TIFFReader().read(stream); if (exifMetadata.directoryCount() == 2) { Directory ifd1 = exifMetadata.getDirectory(1); diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java index 9a13a17f..7ad24538 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReaderTest.java @@ -29,10 +29,10 @@ package com.twelvemonkeys.imageio.plugins.jpeg; import com.twelvemonkeys.imageio.metadata.CompoundDirectory; -import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment; import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; import org.junit.Test; import org.mockito.InOrder; @@ -63,7 +63,7 @@ public class EXIFThumbnailReaderTest extends AbstractThumbnailReaderTest { assertNotNull(segments); assertFalse(segments.isEmpty()); - EXIFReader reader = new EXIFReader(); + TIFFReader reader = new TIFFReader(); InputStream data = segments.get(0).data(); if (data.read() < 0) { throw new AssertionError("EOF!"); diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java index b3f6836e..b3fafd0c 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java @@ -28,22 +28,12 @@ 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.imageio.metadata.MetadataReader; -import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.lang.Validate; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; -import javax.imageio.IIOException; -import javax.imageio.ImageIO; import javax.imageio.stream.ImageInputStream; -import java.io.EOFException; -import java.io.File; import java.io.IOException; -import java.nio.ByteOrder; -import java.nio.charset.Charset; -import java.util.*; /** * EXIFReader @@ -51,528 +41,15 @@ import java.util.*; * @author Harald Kuhr * @author last modified by $Author: haraldk$ * @version $Id: EXIFReader.java,v 1.0 Nov 13, 2009 5:42:51 PM haraldk Exp$ + * + * @deprecated Use com.twelvemonkeys.imageio.metadata.tiff.TIFFReader instead. */ public final class EXIFReader extends MetadataReader { - final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.metadata.exif.debug")); - - static final Collection KNOWN_IFDS = Collections.unmodifiableCollection(Arrays.asList(TIFF.TAG_EXIF_IFD, TIFF.TAG_GPS_IFD, TIFF.TAG_INTEROP_IFD, TIFF.TAG_SUB_IFD)); + private final TIFFReader delegate = new TIFFReader(); @Override public Directory read(final ImageInputStream input) throws IOException { - Validate.notNull(input, "input"); - - byte[] bom = new byte[2]; - input.readFully(bom); - - if (bom[0] == 'I' && bom[1] == 'I') { - input.setByteOrder(ByteOrder.LITTLE_ENDIAN); - } - else if (bom[0] == 'M' && bom[1] == 'M') { - input.setByteOrder(ByteOrder.BIG_ENDIAN); - } - else { - throw new IIOException(String.format("Invalid TIFF byte order mark '%s', expected: 'II' or 'MM'", StringUtil.decode(bom, 0, bom.length, "ASCII"))); - } - - // TODO: BigTiff uses version 43 instead of TIFF's 42, and header is slightly different, see - // http://www.awaresystems.be/imaging/tiff/bigtiff.html - int magic = input.readUnsignedShort(); - if (magic != TIFF.TIFF_MAGIC) { - throw new IIOException(String.format("Wrong TIFF magic in EXIF data: %04x, expected: %04x", magic, TIFF.TIFF_MAGIC)); - } - - long directoryOffset = input.readUnsignedInt(); - - return readDirectory(input, directoryOffset, true); - } - - // TODO: Consider re-writing so that the linked IFD parsing is done externally to the method - protected Directory readDirectory(final ImageInputStream pInput, final long pOffset, final boolean readLinked) throws IOException { - List ifds = new ArrayList<>(); - List entries = new ArrayList<>(); - - pInput.seek(pOffset); - long nextOffset = -1; - - int entryCount; - try { - entryCount = pInput.readUnsignedShort(); - } - catch (EOFException e) { - // Treat EOF here as empty Sub-IFD - entryCount = 0; - } - - for (int i = 0; i < entryCount; i++) { - try { - EXIFEntry entry = readEntry(pInput); - - if (entry != null) { - entries.add(entry); - } - } - catch (IIOException e) { - break; - } - } - - if (readLinked) { - if (nextOffset == -1) { - try { - nextOffset = pInput.readUnsignedInt(); - } - catch (EOFException e) { - // catch EOF here as missing EOF marker - nextOffset = 0; - } - } - - // Read linked IFDs - if (nextOffset != 0) { - CompoundDirectory next = (CompoundDirectory) readDirectory(pInput, nextOffset, true); - - for (int i = 0; i < next.directoryCount(); i++) { - ifds.add((IFD) next.getDirectory(i)); - } - } - } - - // TODO: Consider leaving to client code what sub-IFDs to parse (but always parse TAG_SUB_IFD). - readSubdirectories(pInput, entries, - Arrays.asList(TIFF.TAG_EXIF_IFD, TIFF.TAG_GPS_IFD, TIFF.TAG_INTEROP_IFD, TIFF.TAG_SUB_IFD) - ); - - ifds.add(0, new IFD(entries)); - - return new EXIFDirectory(ifds); - } - - // TODO: Might be better to leave this for client code, as it's tempting go really overboard and support any possible embedded format.. - private void readSubdirectories(ImageInputStream input, List entries, List subIFDIds) throws IOException { - if (subIFDIds == null || subIFDIds.isEmpty()) { - return; - } - - for (int i = 0, entriesSize = entries.size(); i < entriesSize; i++) { - EXIFEntry entry = (EXIFEntry) entries.get(i); - int tagId = (Integer) entry.getIdentifier(); - - if (subIFDIds.contains(tagId)) { - try { - if (KNOWN_IFDS.contains(tagId)) { - long[] pointerOffsets = getPointerOffsets(entry); - List subIFDs = new ArrayList<>(pointerOffsets.length); - - for (long pointerOffset : pointerOffsets) { - CompoundDirectory subDirectory = (CompoundDirectory) readDirectory(input, pointerOffset, false); - - for (int j = 0; j < subDirectory.directoryCount(); j++) { - subIFDs.add((IFD) subDirectory.getDirectory(j)); - } - } - - if (subIFDs.size() == 1) { - // Replace the entry with parsed data - entries.set(i, new EXIFEntry(tagId, subIFDs.get(0), entry.getType())); - } - else { - // Replace the entry with parsed data - entries.set(i, new EXIFEntry(tagId, subIFDs.toArray(new IFD[subIFDs.size()]), entry.getType())); - } - } - } - catch (IIOException e) { - if (DEBUG) { - // TODO: Issue warning without crashing...? - System.err.println("Error parsing sub-IFD: " + tagId); - e.printStackTrace(); - } - } - } - } - } - - private long[] getPointerOffsets(final Entry entry) throws IIOException { - long[] offsets; - Object value = entry.getValue(); - - if (value instanceof Byte) { - offsets = new long[] {(Byte) value & 0xff}; - } - else if (value instanceof Short) { - offsets = new long[] {(Short) value & 0xffff}; - } - else if (value instanceof Integer) { - offsets = new long[] {(Integer) value & 0xffffffffL}; - } - else if (value instanceof Long) { - offsets = new long[] {(Long) value}; - } - else if (value instanceof long[]) { - offsets = (long[]) value; - } - else { - throw new IIOException(String.format("Unknown pointer type: %s", (value != null - ? value.getClass() - : null))); - } - - return offsets; - } - - private EXIFEntry readEntry(final ImageInputStream pInput) throws IOException { - // TODO: BigTiff entries are different - int tagId = pInput.readUnsignedShort(); - short type = pInput.readShort(); - - int count = pInput.readInt(); // Number of values - - // It's probably a spec violation to have count 0, but we'll be lenient about it - if (count < 0) { - throw new IIOException(String.format("Illegal count %d for tag %s type %s @%08x", count, tagId, type, pInput.getStreamPosition())); - } - - if (type <= 0 || type > 13) { - pInput.skipBytes(4); // read Value - - // Invalid tag, this is just for debugging - long offset = pInput.getStreamPosition() - 12l; - - if (DEBUG) { - System.err.printf("Bad EXIF data @%08x\n", pInput.getStreamPosition()); - System.err.println("tagId: " + tagId + (tagId <= 0 ? " (INVALID)" : "")); - System.err.println("type: " + type + " (INVALID)"); - System.err.println("count: " + count); - - pInput.mark(); - pInput.seek(offset); - - try { - byte[] bytes = new byte[8 + Math.min(120, Math.max(24, count))]; - int len = pInput.read(bytes); - - if (DEBUG) { - System.err.print(HexDump.dump(offset, bytes, 0, len)); - System.err.println(len < count ? "[...]" : ""); - } - } - finally { - pInput.reset(); - } - } - return null; - } - - int valueLength = getValueLength(type, count); - - Object value; - // TODO: For BigTiff allow size > 4 && <= 8 in addition - if (valueLength > 0 && valueLength <= 4) { - value = readValueInLine(pInput, type, count); - pInput.skipBytes(4 - valueLength); - } - else { - long valueOffset = pInput.readUnsignedInt(); // This is the *value* iff the value size is <= 4 bytes - value = readValueAt(pInput, valueOffset, type, count); - } - - return new EXIFEntry(tagId, value, type); - } - - private Object readValueAt(final ImageInputStream pInput, final long pOffset, final short pType, final int pCount) throws IOException { - long pos = pInput.getStreamPosition(); - try { - pInput.seek(pOffset); - return readValue(pInput, pType, pCount); - } - catch (EOFException e) { - // TODO: Add warning listener API and report problem to client code - return e; - } - finally { - pInput.seek(pos); - } - } - - private Object readValueInLine(final ImageInputStream pInput, final short pType, final int pCount) throws IOException { - return readValue(pInput, pType, pCount); - } - - 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(); - - switch (pType) { - case TIFF.TYPE_ASCII: - // TODO: This might be UTF-8 or ISO-8859-x, even though spec says NULL-terminated 7 bit ASCII - // TODO: Fail if unknown chars, try parsing with ISO-8859-1 or file.encoding - if (pCount == 0) { - return ""; - } - byte[] ascii = new byte[pCount]; - pInput.readFully(ascii); - int len = ascii[ascii.length - 1] == 0 ? ascii.length - 1 : ascii.length; - return StringUtil.decode(ascii, 0, len, "UTF-8"); // UTF-8 is ASCII compatible - case TIFF.TYPE_BYTE: - if (pCount == 1) { - return pInput.readUnsignedByte(); - } - // else fall through - case TIFF.TYPE_SBYTE: - if (pCount == 1) { - return pInput.readByte(); - } - // else fall through - case TIFF.TYPE_UNDEFINED: - byte[] bytes = new byte[pCount]; - pInput.readFully(bytes); - - // NOTE: We don't change (unsigned) BYTE array wider Java type, as most often BYTE array means - // binary data and we want to keep that as a byte array for clients to parse further - - return bytes; - case TIFF.TYPE_SHORT: - if (pCount == 1) { - return pInput.readUnsignedShort(); - } - case TIFF.TYPE_SSHORT: - if (pCount == 1) { - return pInput.readShort(); - } - - short[] shorts = new short[pCount]; - pInput.readFully(shorts, 0, shorts.length); - - if (pType == TIFF.TYPE_SHORT) { - int[] ints = new int[pCount]; - for (int i = 0; i < pCount; i++) { - ints[i] = shorts[i] & 0xffff; - } - - return ints; - } - - return shorts; - case TIFF.TYPE_IFD: - case TIFF.TYPE_LONG: - if (pCount == 1) { - return pInput.readUnsignedInt(); - } - case TIFF.TYPE_SLONG: - if (pCount == 1) { - return pInput.readInt(); - } - - int[] ints = new int[pCount]; - pInput.readFully(ints, 0, ints.length); - - if (pType == TIFF.TYPE_LONG || pType == TIFF.TYPE_IFD) { - long[] longs = new long[pCount]; - for (int i = 0; i < pCount; i++) { - longs[i] = ints[i] & 0xffffffffL; - } - - return longs; - } - - return ints; - case TIFF.TYPE_FLOAT: - if (pCount == 1) { - return pInput.readFloat(); - } - - float[] floats = new float[pCount]; - pInput.readFully(floats, 0, floats.length); - return floats; - case TIFF.TYPE_DOUBLE: - if (pCount == 1) { - return pInput.readDouble(); - } - - double[] doubles = new double[pCount]; - pInput.readFully(doubles, 0, doubles.length); - return doubles; - - case TIFF.TYPE_RATIONAL: - if (pCount == 1) { - return createSafeRational(pInput.readUnsignedInt(), pInput.readUnsignedInt()); - } - - Rational[] rationals = new Rational[pCount]; - for (int i = 0; i < rationals.length; i++) { - rationals[i] = createSafeRational(pInput.readUnsignedInt(), pInput.readUnsignedInt()); - } - - return rationals; - case TIFF.TYPE_SRATIONAL: - if (pCount == 1) { - return createSafeRational(pInput.readInt(), pInput.readInt()); - } - - Rational[] srationals = new Rational[pCount]; - for (int i = 0; i < srationals.length; i++) { - srationals[i] = createSafeRational(pInput.readInt(), pInput.readInt()); - } - - return srationals; - - // BigTiff: - case 16: // LONG8 - case 17: // SLONG8 - case 18: // IFD8 - // TODO: Assert BigTiff (version == 43) - - if (pCount == 1) { - long val = pInput.readLong(); - if (pType != 17 && val < 0) { - throw new IIOException(String.format("Value > %s", Long.MAX_VALUE)); - } - return val; - } - - long[] longs = new long[pCount]; - for (int i = 0; i < pCount; i++) { - longs[i] = pInput.readLong(); - } - - return longs; - - default: - // Spec says skip unknown values - return new Unknown(pType, pCount, pos); - } - } - - private static Rational createSafeRational(final long numerator, final long denominator) throws IOException { - if (denominator == 0) { - // Bad data. - return Rational.NaN; - } - - return new Rational(numerator, denominator); - } - - static int getValueLength(final int pType, final int pCount) { - if (pType > 0 && pType < TIFF.TYPE_LENGTHS.length) { - return TIFF.TYPE_LENGTHS[pType] * pCount; - } - - return -1; - } - - public static void main(String[] args) throws IOException { - EXIFReader reader = new EXIFReader(); - ImageInputStream stream = ImageIO.createImageInputStream(new File(args[0])); - - long pos = 0; - if (args.length > 1) { - if (args[1].startsWith("0x")) { - pos = Integer.parseInt(args[1].substring(2), 16); - } - else { - pos = Long.parseLong(args[1]); - } - - stream.setByteOrder(pos < 0 ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN); - - pos = Math.abs(pos); - - stream.seek(pos); - } - - try { - Directory directory; - - if (args.length > 1) { - directory = reader.readDirectory(stream, pos, false); - } - else { - directory = reader.read(stream); - } - - for (Entry entry : directory) { - System.err.println(entry); - - Object value = entry.getValue(); - if (value instanceof byte[]) { - byte[] bytes = (byte[]) value; - System.err.println(HexDump.dump(0, bytes, 0, Math.min(bytes.length, 128))); - } - } - } - finally { - stream.close(); - } - } - - ////////////////////// - // TODO: Stream based hex dump util? - public static class HexDump { - private HexDump() { - } - - private static final int WIDTH = 32; - - public static String dump(byte[] bytes) { - return dump(0, bytes, 0, bytes.length); - } - - public static String dump(long offset, byte[] bytes, int off, int len) { - StringBuilder builder = new StringBuilder(); - - int i; - for (i = 0; i < len; i++) { - if (i % WIDTH == 0) { - if (i > 0) { - builder.append("\n"); - } - builder.append(String.format("%08x: ", i + off + offset)); - } - else if (i > 0 && i % 2 == 0) { - builder.append(" "); - } - - builder.append(String.format("%02x", bytes[i + off])); - - int next = i + 1; - if (next % WIDTH == 0 || next == len) { - int leftOver = (WIDTH - (next % WIDTH)) % WIDTH; - - if (leftOver != 0) { - // Pad: 5 spaces for every 2 bytes... Special care if padding is non-even. - int pad = leftOver / 2; - - if (len % 2 != 0) { - builder.append(" "); - } - - for (int j = 0; j < pad; j++) { - builder.append(" "); - } - } - - builder.append(" "); - builder.append(toAsciiString(bytes, next - (WIDTH - leftOver) + off, next + off)); - } - } - - return builder.toString(); - } - - private static String toAsciiString(final byte[] bytes, final int from, final int to) { - byte[] range = Arrays.copyOfRange(bytes, from, to); - - for (int i = 0; i < range.length; i++) { - if (range[i] < 32 || range[i] > 126) { - range[i] = '.'; // Unreadable char - } - } - - return new String(range, Charset.forName("ascii")); - } + return delegate.read(input); } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriter.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriter.java index af0ac041..ec1601b3 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriter.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, Harald Kuhr + * Copyright (c) 2009, Harald Kuhr * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,20 +28,14 @@ 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.imageio.metadata.MetadataWriter; -import com.twelvemonkeys.lang.Validate; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFWriter; -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.nio.charset.StandardCharsets; -import java.util.*; +import java.util.Collection; /** * EXIFWriter @@ -49,430 +43,19 @@ import java.util.*; * @author Harald Kuhr * @author last modified by $Author: haraldk$ * @version $Id: EXIFWriter.java,v 1.0 17.07.13 10:20 haraldk Exp$ + * + * @deprecated Use com.twelvemonkeys.imageio.metadata.tiff.TIFFWriter instead. */ public final class EXIFWriter extends MetadataWriter { - static final int WORD_LENGTH = 2; - static final int LONGWORD_LENGTH = 4; - static final int ENTRY_LENGTH = 12; - - public boolean write(final Collection entries, final ImageOutputStream stream) throws IOException { - return write(new IFD(entries), stream); - } + private final TIFFWriter delegate = new TIFFWriter(); @Override 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; + return delegate.write(directory, stream); } - 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 boolean write(final Collection entries, final ImageOutputStream stream) throws IOException { + return delegate.write(entries, stream); } - - public long writeIFD(final Collection entries, final ImageOutputStream stream) throws IOException { - return writeIFD(new IFD(entries), stream, false); - } - - private long writeIFD(final Directory original, final ImageOutputStream stream, final boolean isSubIFD) throws IOException { - // TIFF spec says tags should be in increasing order, enforce that when writing - Directory ordered = ensureOrderedDirectory(original); - - // Compute space needed for extra storage first, then write the offset to the IFD, so that the layout is: - // IFD offset - // - // IFD entries (values/offsets) - long dataOffset = stream.getStreamPosition(); - long dataSize = computeDataSize(ordered); - - // Offset to this IFD - final long ifdOffset = stream.getStreamPosition() + dataSize + LONGWORD_LENGTH; - - if (!isSubIFD) { - stream.writeInt(assertIntegerOffset(ifdOffset)); - dataOffset += LONGWORD_LENGTH; - - // Seek to offset - stream.seek(ifdOffset); - } - else { - dataOffset += WORD_LENGTH + ordered.size() * ENTRY_LENGTH; - } - - // Write directory - stream.writeShort(ordered.size()); - - for (Entry entry : ordered) { - // Write tag id - stream.writeShort((Integer) entry.getIdentifier()); - // Write tag type - stream.writeShort(getType(entry)); - // Write value count - stream.writeInt(getCount(entry)); - - // Write value - if (entry.getValue() instanceof Directory) { - // TODO: This could possibly be a compound directory, in which case the count should be > 1 - stream.writeInt(assertIntegerOffset(dataOffset)); - long streamPosition = stream.getStreamPosition(); - stream.seek(dataOffset); - Directory subIFD = (Directory) entry.getValue(); - writeIFD(subIFD, stream, true); - dataOffset += computeDataSize(subIFD); - stream.seek(streamPosition); - } - else { - dataOffset += writeValue(entry, dataOffset, stream); - } - } - - return ifdOffset; - } - - public long computeIFDSize(final Collection directory) { - return WORD_LENGTH + computeDataSize(new IFD(directory)) + directory.size() * ENTRY_LENGTH; - } - - public long computeIFDOffsetSize(final Collection directory) { - return computeDataSize(new IFD(directory)) + LONGWORD_LENGTH; - } - - private long computeDataSize(final Directory directory) { - long dataSize = 0; - - for (Entry entry : directory) { - int length = EXIFReader.getValueLength(getType(entry), getCount(entry)); - - if (length < 0) { - throw new IllegalArgumentException(String.format("Unknown size for entry %s", entry)); - } - - if (length > LONGWORD_LENGTH) { - dataSize += length; - } - - if (entry.getValue() instanceof Directory) { - Directory subIFD = (Directory) entry.getValue(); - long subIFDSize = WORD_LENGTH + subIFD.size() * ENTRY_LENGTH + computeDataSize(subIFD); - dataSize += subIFDSize; - } - } - - return dataSize; - } - - private Directory ensureOrderedDirectory(final Directory directory) { - if (!isSorted(directory)) { - List entries = new ArrayList<>(directory.size()); - - for (Entry entry : directory) { - entries.add(entry); - } - - Collections.sort(entries, new Comparator() { - public int compare(Entry left, Entry right) { - return (Integer) left.getIdentifier() - (Integer) right.getIdentifier(); - } - }); - - return new IFD(entries); - } - - return directory; - } - - private boolean isSorted(final Directory directory) { - int lastTag = 0; - - for (Entry entry : directory) { - int tag = ((Integer) entry.getIdentifier()) & 0xffff; - - if (tag < lastTag) { - return false; - } - - lastTag = tag; - } - - return true; - } - - private long writeValue(final Entry entry, final long dataOffset, final 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(final Entry entry) { - Object value = entry.getValue(); - return value instanceof String ? ((String) value).getBytes(Charset.forName("UTF-8")).length + 1 : entry.valueCount(); - } - - private void writeValueInline(final Object value, final short type, final ImageOutputStream stream) throws IOException { - if (value.getClass().isArray()) { - switch (type) { - case TIFF.TYPE_UNDEFINED: - case TIFF.TYPE_BYTE: - case TIFF.TYPE_SBYTE: - stream.write((byte[]) value); - break; - - case TIFF.TYPE_SHORT: - case TIFF.TYPE_SSHORT: - 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: - case TIFF.TYPE_SLONG: - 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 LONG: " + value.getClass()); - } - - stream.writeInts(ints, 0, ints.length); - break; - - case TIFF.TYPE_RATIONAL: - case TIFF.TYPE_SRATIONAL: - Rational[] rationals = (Rational[]) value; - for (Rational rational : rationals) { - stream.writeInt((int) rational.numerator()); - stream.writeInt((int) rational.denominator()); - } - - break; - - case TIFF.TYPE_FLOAT: - float[] floats; - - if (value instanceof float[]) { - floats = (float[]) value; - } - else { - throw new IllegalArgumentException("Unsupported type for TIFF FLOAT: " + value.getClass()); - } - - stream.writeFloats(floats, 0, floats.length); - - break; - - case TIFF.TYPE_DOUBLE: - double[] doubles; - - if (value instanceof double[]) { - doubles = (double[]) value; - } - else { - throw new IllegalArgumentException("Unsupported type for TIFF FLOAT: " + value.getClass()); - } - - stream.writeDoubles(doubles, 0, doubles.length); - - break; - - 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: - case TIFF.TYPE_SBYTE: - case TIFF.TYPE_UNDEFINED: - stream.writeByte(((Number) value).intValue()); - break; - case TIFF.TYPE_ASCII: - byte[] bytes = ((String) value).getBytes(StandardCharsets.UTF_8); - stream.write(bytes); - stream.write(0); - break; - case TIFF.TYPE_SHORT: - case TIFF.TYPE_SSHORT: - stream.writeShort(((Number) value).intValue()); - break; - case TIFF.TYPE_LONG: - case TIFF.TYPE_SLONG: - stream.writeInt(((Number) value).intValue()); - break; - case TIFF.TYPE_RATIONAL: - case TIFF.TYPE_SRATIONAL: - Rational rational = (Rational) value; - stream.writeInt((int) rational.numerator()); - stream.writeInt((int) rational.denominator()); - break; - case TIFF.TYPE_FLOAT: - stream.writeFloat(((Number) value).floatValue()); - break; - case TIFF.TYPE_DOUBLE: - stream.writeDouble(((Number) value).doubleValue()); - break; - - default: - throw new IllegalArgumentException("Unsupported TIFF type: " + type); - } - } - } - - private void writeValueAt(final long dataOffset, final Object value, final short type, final 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(final Entry entry) { - // TODO: What a MESS! Rewrite and expose EXIFEntry as TIFFEntry or so... - - // For internal entries use type directly - if (entry instanceof EXIFEntry) { - EXIFEntry exifEntry = (EXIFEntry) entry; - return exifEntry.getType(); - } - - // For other entries, use name if it matches - String typeName = entry.getTypeName(); - - if (typeName != null) { - for (int i = 1; i < TIFF.TYPE_NAMES.length; i++) { - if (typeName.equals(TIFF.TYPE_NAMES[i])) { - return (short) i; - } - } - } - - // Otherwise, fall back to the native Java type - 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; - } -} +} \ No newline at end of file diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/Rational.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/Rational.java index b0e0a247..52391289 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/Rational.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/Rational.java @@ -38,191 +38,84 @@ package com.twelvemonkeys.imageio.metadata.exif; * Represents a rational number with a {@code long} numerator and {@code long} denominator. * Rational numbers are stored in reduced form with the sign stored with the numerator. * Rationals are immutable. - *

- * Adapted from sample code featured in - * "Intro to Programming in Java: An Interdisciplinary Approach" (Addison Wesley) - * by Robert Sedgewick and Kevin Wayne. Permission granted to redistribute under BSD license. * * @author Harald Kuhr - * @author Robert Sedgewick and Kevin Wayne (original version) * @author last modified by $Author: haraldk$ * @version $Id: Rational.java,v 1.0 Nov 18, 2009 1:12:00 AM haraldk Exp$ + * + * @deprecated Use com.twelvemonkeys.imageio.metadata.tiff.Rational instead. */ +@SuppressWarnings("deprecation") public final class Rational extends Number implements Comparable { - // TODO: Document public API - // TODO: Move to com.tm.lang? - // Inspired by http://www.cs.princeton.edu/introcs/92symbolic/Rational.java.html and java.lang.Integer - static final Rational ZERO = new Rational(0, 1); - static final Rational NaN = new Rational(); // TODO: This field needs thoughts/tests/spec/consistency check, see Float.NaN - - private final long numerator; - private final long denominator; - - private Rational() { - numerator = 0; - denominator = 0; - } + private final com.twelvemonkeys.imageio.metadata.tiff.Rational delegate; public Rational(final long pNumber) { - this(pNumber, 1); + this(new com.twelvemonkeys.imageio.metadata.tiff.Rational(pNumber, 1)); } public Rational(final long pNumerator, final long pDenominator) { - if (pDenominator == 0) { - throw new IllegalArgumentException("denominator == 0"); - } - if (pNumerator == Long.MIN_VALUE || pDenominator == Long.MIN_VALUE) { - throw new IllegalArgumentException("value == Long.MIN_VALUE"); - } - - // Reduce fractions - long gcd = gcd(pNumerator, pDenominator); - long num = pNumerator / gcd; - long den = pDenominator / gcd; - - numerator = pDenominator >= 0 ? num : -num; - denominator = pDenominator >= 0 ? den : -den; + this(new com.twelvemonkeys.imageio.metadata.tiff.Rational(pNumerator, pDenominator)); } - private static long gcd(final long m, final long n) { - if (m < 0) { - return gcd(n, -m); - } - - return n == 0 ? m : gcd(n, m % n); - } - - private static long lcm(final long m, final long n) { - if (m < 0) { - return lcm(n, -m); - } - - return m * (n / gcd(m, n)); // parentheses important to avoid overflow + private Rational(final com.twelvemonkeys.imageio.metadata.tiff.Rational delegate) { + this.delegate = delegate; } public long numerator() { - return numerator; + return delegate.numerator(); } public long denominator() { - return denominator; + return delegate.denominator(); } - /// Number implementation + @Override + public byte byteValue() { + return delegate.byteValue(); + } + + @Override + public short shortValue() { + return delegate.shortValue(); + } @Override public int intValue() { - return (int) doubleValue(); + return delegate.intValue(); } @Override public long longValue() { - return (long) doubleValue(); + return delegate.longValue(); } @Override public float floatValue() { - return (float) doubleValue(); + return delegate.floatValue(); } @Override public double doubleValue() { - if (this == NaN) { - return Double.NaN; - } - - return numerator / (double) denominator; + return delegate.doubleValue(); } - /// Comparable implementation - - public int compareTo(final Rational pOther) { - double thisVal = doubleValue(); - double otherVal = pOther.doubleValue(); - - return thisVal < otherVal ? -1 : thisVal == otherVal ? 0 : 1; + public int compareTo(Rational pOther) { + return delegate.compareTo(pOther.delegate); } - /// Object overrides - @Override public int hashCode() { - return Float.floatToIntBits(floatValue()); + return delegate.hashCode(); } @Override public boolean equals(final Object pOther) { - return pOther == this || pOther instanceof Rational && compareTo((Rational) pOther) == 0; + return pOther == this || pOther instanceof Rational && delegate.equals(((Rational) pOther).delegate); + } @Override public String toString() { - if (this == NaN) { - return "NaN"; - } - - return denominator == 1 ? Long.toString(numerator) : String.format("%s/%s", numerator, denominator); - } - - /// Operations (adapted from http://www.cs.princeton.edu/introcs/92symbolic/Rational.java.html) - // TODO: Naming! multiply/divide/add/subtract or times/divides/plus/minus - - // return a * b, staving off overflow as much as possible by cross-cancellation - public Rational times(final Rational pOther) { - // special cases - if (equals(ZERO) || pOther.equals(ZERO)) { - return ZERO; - } - - // reduce p1/q2 and p2/q1, then multiply, where a = p1/q1 and b = p2/q2 - Rational c = new Rational(numerator, pOther.denominator); - Rational d = new Rational(pOther.numerator, denominator); - - return new Rational(c.numerator * d.numerator, c.denominator * d.denominator); - } - - // return a + b, staving off overflow - public Rational plus(final Rational pOther) { - // special cases - if (equals(ZERO)) { - return pOther; - } - if (pOther.equals(ZERO)) { - return this; - } - - // Find gcd of numerators and denominators - long f = gcd(numerator, pOther.numerator); - long g = gcd(denominator, pOther.denominator); - - // add cross-product terms for numerator - // multiply back in - return new Rational( - ((numerator / f) * (pOther.denominator / g) + (pOther.numerator / f) * (denominator / g)) * f, - lcm(denominator, pOther.denominator) - ); - } - - // return -a - public Rational negate() { - return new Rational(-numerator, denominator); - } - - // return a - b - public Rational minus(final Rational pOther) { - return plus(pOther.negate()); - } - - public Rational reciprocal() { - return new Rational(denominator, numerator); - } - - // return a / b - public Rational divides(final Rational pOther) { - if (pOther.equals(ZERO)) { - throw new ArithmeticException("/ by zero"); - } - - return times(pOther.reciprocal()); + return delegate.toString(); } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java index aeca1fd0..17b1c696 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java @@ -34,207 +34,8 @@ package com.twelvemonkeys.imageio.metadata.exif; * @author Harald Kuhr * @author last modified by $Author: haraldk$ * @version $Id: TIFF.java,v 1.0 Nov 15, 2009 3:02:24 PM haraldk Exp$ + * + * @deprecated Use com.twelvemonkeys.imageio.metadata.tiff.TIFF instead. */ -@SuppressWarnings("UnusedDeclaration") -public interface TIFF { - short BYTE_ORDER_MARK_BIG_ENDIAN = ('M' << 8) | 'M'; - short BYTE_ORDER_MARK_LITTLE_ENDIAN = ('I' << 8) | 'I'; - - int TIFF_MAGIC = 42; - - short TYPE_BYTE = 1; - short TYPE_ASCII = 2; - short TYPE_SHORT = 3; - short TYPE_LONG = 4; - short TYPE_RATIONAL = 5; - - short TYPE_SBYTE = 6; - short TYPE_UNDEFINED = 7; - short TYPE_SSHORT = 8; - short TYPE_SLONG = 9; - short TYPE_SRATIONAL = 10; - short TYPE_FLOAT = 11; - short TYPE_DOUBLE = 12; - short TYPE_IFD = 13; - /* - 1 = BYTE 8-bit unsigned integer. - 2 = ASCII 8-bit byte that contains a 7-bit ASCII code; the last byte - must be NUL (binary zero). - 3 = SHORT 16-bit (2-byte) unsigned integer. - 4 = LONG 32-bit (4-byte) unsigned integer. - 5 = RATIONAL Two LONGs: the first represents the numerator of a - fraction; the second, the denominator. - - TIFF 6.0 and above: - 6 = SBYTE An 8-bit signed (twos-complement) integer. - 7 = UNDEFINED An 8-bit byte that may contain anything, depending on - the definition of the field. - 8 = SSHORT A 16-bit (2-byte) signed (twos-complement) integer. - 9 = SLONG A 32-bit (4-byte) signed (twos-complement) integer. - 10 = SRATIONAL Two SLONGs: the first represents the numerator of a - fraction, the second the denominator. - 11 = FLOAT Single precision (4-byte) IEEE format. - 12 = DOUBLE Double precision (8-byte) IEEE format. - - See http://www.awaresystems.be/imaging/tiff/tifftags/subifds.html - 13 = IFD, same as LONG - - TODO: BigTiff specifies more types - See http://www.awaresystems.be/imaging/tiff/bigtiff.html, http://www.remotesensing.org/libtiff/bigtiffdesign.html - (what about 14-15??) - 16 = TIFF_LONG8, being unsigned 8byte integer - 17 = TIFF_SLONG8, being signed 8byte integer - 18 = TIFF_IFD8, being a new unsigned 8byte IFD offset. - Should probably all map to Java long (and fail if high bit is set for the unsigned types???) - */ - String[] TYPE_NAMES = { - null, - "BYTE", "ASCII", "SHORT", "LONG", "RATIONAL", - "SBYTE", "UNDEFINED", "SSHORT", "SLONG", "SRATIONAL", "FLOAT", "DOUBLE", - "IFD", - null, null, - "LONG8", "SLONG8", "IFD8" - }; - /** Length of the corresponding type, in bytes. */ - int[] TYPE_LENGTHS = { - -1, - 1, 1, 2, 4, 8, - 1, 1, 2, 4, 8, 4, 8, - 4, - -1, -1, - 8, 8, 8 - }; - - /// EXIF defined TIFF tags - - int TAG_EXIF_IFD = 34665; - int TAG_GPS_IFD = 34853; - int TAG_INTEROP_IFD = 40965; - - /// A. Tags relating to image data structure: - - int TAG_IMAGE_WIDTH = 256; - int TAG_IMAGE_HEIGHT = 257; - int TAG_BITS_PER_SAMPLE = 258; - int TAG_COMPRESSION = 259; - int TAG_PHOTOMETRIC_INTERPRETATION = 262; - int TAG_FILL_ORDER = 266; - int TAG_ORIENTATION = 274; - int TAG_SAMPLES_PER_PIXEL = 277; - int TAG_PLANAR_CONFIGURATION = 284; - int TAG_SAMPLE_FORMAT = 339; - int TAG_YCBCR_SUB_SAMPLING = 530; - int TAG_YCBCR_POSITIONING = 531; - int TAG_X_RESOLUTION = 282; - int TAG_Y_RESOLUTION = 283; - int TAG_X_POSITION = 286; - int TAG_Y_POSITION = 287; - int TAG_RESOLUTION_UNIT = 296; - - /// B. Tags relating to recording offset - - int TAG_STRIP_OFFSETS = 273; - int TAG_ROWS_PER_STRIP = 278; - int TAG_STRIP_BYTE_COUNTS = 279; - int TAG_FREE_OFFSETS = 288; // "Not recommended for general interchange." - // "Old-style" JPEG (still used as EXIF thumbnail) - int TAG_JPEG_INTERCHANGE_FORMAT = 513; - int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514; - - int TAG_GROUP3OPTIONS = 292; - int TAG_GROUP4OPTIONS = 293; - - /// C. Tags relating to image data characteristics - - int TAG_TRANSFER_FUNCTION = 301; - int TAG_PREDICTOR = 317; - int TAG_WHITE_POINT = 318; - int TAG_PRIMARY_CHROMATICITIES = 319; - int TAG_COLOR_MAP = 320; - int TAG_INK_SET = 332; - int TAG_INK_NAMES = 333; - int TAG_NUMBER_OF_INKS = 334; - int TAG_EXTRA_SAMPLES = 338; - int TAG_TRANSFER_RANGE = 342; - int TAG_YCBCR_COEFFICIENTS = 529; - int TAG_REFERENCE_BLACK_WHITE = 532; - - /// D. Other tags - - int TAG_DATE_TIME = 306; - int TAG_DOCUMENT_NAME = 269; - int TAG_IMAGE_DESCRIPTION = 270; - int TAG_MAKE = 271; - int TAG_MODEL = 272; - int TAG_PAGE_NAME = 285; - int TAG_PAGE_NUMBER = 297; - int TAG_SOFTWARE = 305; - int TAG_ARTIST = 315; - int TAG_HOST_COMPUTER = 316; - int TAG_COPYRIGHT = 33432; - - int TAG_SUBFILE_TYPE = 254; - int TAG_OLD_SUBFILE_TYPE = 255; // Deprecated NO NOT WRITE! - int TAG_SUB_IFD = 330; - - /** - * XMP record. - * @see com.twelvemonkeys.imageio.metadata.xmp.XMP - */ - int TAG_XMP = 700; - - /** - * IPTC record. - * @see com.twelvemonkeys.imageio.metadata.iptc.IPTC - */ - int TAG_IPTC = 33723; - - /** - * Photoshop image resources. - * @see com.twelvemonkeys.imageio.metadata.psd.PSD - */ - int TAG_PHOTOSHOP = 34377; - - /** - * Photoshop layer and mask information (byte order follows TIFF container). - * Layer and mask information found in a typical layered Photoshop file. - * Starts with a character string of "Adobe Photoshop Document Data Block" - * (or "Adobe Photoshop Document Data V0002" for PSB) - * including the null termination character. - * @see com.twelvemonkeys.imageio.metadata.psd.PSD - */ - int TAG_PHOTOSHOP_IMAGE_SOURCE_DATA = 37724; - - int TAG_PHOTOSHOP_ANNOTATIONS = 50255; - - /** - * ICC Color Profile. - * @see java.awt.color.ICC_Profile - */ - int TAG_ICC_PROFILE = 34675; - - // Microsoft Office Document Imaging (MODI) - // http://msdn.microsoft.com/en-us/library/aa167596%28office.11%29.aspx - int TAG_MODI_BLC = 34718; - int TAG_MODI_VECTOR = 34719; - int TAG_MODI_PTC = 34720; - - // http://blogs.msdn.com/b/openspecification/archive/2009/12/08/details-of-three-tiff-tag-extensions-that-microsoft-office-document-imaging-modi-software-may-write-into-the-tiff-files-it-generates.aspx - int TAG_MODI_PLAIN_TEXT = 37679; - int TAG_MODI_OLE_PROPERTY_SET = 37680; - int TAG_MODI_TEXT_POS_INFO = 37681; - - int TAG_TILE_WIDTH = 322; - int TAG_TILE_HEIGTH = 323; - int TAG_TILE_OFFSETS = 324; - int TAG_TILE_BYTE_COUNTS = 325; - - // JPEG - int TAG_JPEG_TABLES = 347; - - // "Old-style" JPEG (Obsolete) DO NOT WRITE! - int TAG_OLD_JPEG_PROC = 512; - int TAG_OLD_JPEG_Q_TABLES = 519; - int TAG_OLD_JPEG_DC_TABLES = 520; - int TAG_OLD_JPEG_AC_TABLES = 521; +public interface TIFF extends com.twelvemonkeys.imageio.metadata.tiff.TIFF { } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java index da6e08b5..ea8db62f 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java @@ -30,9 +30,9 @@ package com.twelvemonkeys.imageio.metadata.jpeg; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; -import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; import com.twelvemonkeys.imageio.metadata.psd.PSD; import com.twelvemonkeys.imageio.metadata.psd.PSDReader; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; import com.twelvemonkeys.imageio.metadata.xmp.XMP; import com.twelvemonkeys.imageio.metadata.xmp.XMPReader; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; @@ -42,7 +42,10 @@ import javax.imageio.ImageIO; import javax.imageio.stream.ImageInputStream; import java.awt.color.ICC_ColorSpace; import java.awt.color.ICC_Profile; -import java.io.*; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; import java.nio.charset.Charset; import java.util.*; @@ -279,13 +282,13 @@ public final class JPEGSegmentUtil { ImageInputStream stream = new ByteArrayImageInputStream(segment.data, segment.offset() + 1, segment.length() - 1); // Root entry is TIFF, that contains the EXIF sub-IFD - Directory tiff = new EXIFReader().read(stream); + Directory tiff = new TIFFReader().read(stream); System.err.println("EXIF: " + tiff); } else if (XMP.NS_XAP.equals(segment.identifier())) { Directory xmp = new XMPReader().read(new ByteArrayImageInputStream(segment.data, segment.offset(), segment.length())); System.err.println("XMP: " + xmp); - System.err.println(EXIFReader.HexDump.dump(segment.data)); + System.err.println(TIFFReader.HexDump.dump(segment.data)); } else if ("Photoshop 3.0".equals(segment.identifier())) { // TODO: The "Photoshop 3.0" segment contains several image resources, of which one might contain @@ -298,13 +301,13 @@ public final class JPEGSegmentUtil { System.err.println("colorSpace: " + colorSpace); } System.err.println("PSD: " + psd); - System.err.println(EXIFReader.HexDump.dump(segment.data)); + System.err.println(TIFFReader.HexDump.dump(segment.data)); } else if ("ICC_PROFILE".equals(segment.identifier())) { // Skip } else { - System.err.println(EXIFReader.HexDump.dump(segment.data)); + System.err.println(TIFFReader.HexDump.dump(segment.data)); } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/IFD.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/IFD.java similarity index 90% rename from imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/IFD.java rename to imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/IFD.java index 0340517d..6f7b2a09 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/IFD.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/IFD.java @@ -26,7 +26,7 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.twelvemonkeys.imageio.metadata.exif; +package com.twelvemonkeys.imageio.metadata.tiff; import com.twelvemonkeys.imageio.metadata.AbstractDirectory; import com.twelvemonkeys.imageio.metadata.Entry; @@ -34,14 +34,14 @@ import com.twelvemonkeys.imageio.metadata.Entry; import java.util.Collection; /** - * IFD + * Represents a TIFF Image File Directory. * * @author Harald Kuhr * @author last modified by $Author: haraldk$ * @version $Id: IFD.java,v 1.0 23.12.11 16:24 haraldk Exp$ */ -final class IFD extends AbstractDirectory { - protected IFD(final Collection pEntries) { +public final class IFD extends AbstractDirectory { + public IFD(final Collection pEntries) { super(pEntries); } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/Rational.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/Rational.java new file mode 100644 index 00000000..987253cc --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/Rational.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2009, 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. + */ + +/* + * Adapted from sample code featured in + * "Intro to Programming in Java: An Interdisciplinary Approach" (Addison Wesley) + * by Robert Sedgewick and Kevin Wayne. Permission granted to redistribute under BSD license. + */ + +package com.twelvemonkeys.imageio.metadata.tiff; + +/** + * Represents a rational number with a {@code long} numerator and {@code long} denominator. + * Rational numbers are stored in reduced form with the sign stored with the numerator. + * Rationals are immutable. + *

+ * Adapted from sample code featured in + * "Intro to Programming in Java: An Interdisciplinary Approach" (Addison Wesley) + * by Robert Sedgewick and Kevin Wayne. Permission granted to redistribute under BSD license. + * + * @author Harald Kuhr + * @author Robert Sedgewick and Kevin Wayne (original version) + * @author last modified by $Author: haraldk$ + * @version $Id: Rational.java,v 1.0 Nov 18, 2009 1:12:00 AM haraldk Exp$ + */ +public final class Rational extends Number implements Comparable { + // TODO: Document public API + // TODO: Move to com.tm.lang? + // Inspired by http://www.cs.princeton.edu/introcs/92symbolic/Rational.java.html and java.lang.Integer + static final Rational ZERO = new Rational(0, 1); + static final Rational NaN = new Rational(); // TODO: This field needs thoughts/tests/spec/consistency check, see Float.NaN + + private final long numerator; + private final long denominator; + + private Rational() { + numerator = 0; + denominator = 0; + } + + public Rational(final long pNumber) { + this(pNumber, 1); + } + + public Rational(final long pNumerator, final long pDenominator) { + if (pDenominator == 0) { + throw new IllegalArgumentException("denominator == 0"); + } + if (pNumerator == Long.MIN_VALUE || pDenominator == Long.MIN_VALUE) { + throw new IllegalArgumentException("value == Long.MIN_VALUE"); + } + + // Reduce fractions + long gcd = gcd(pNumerator, pDenominator); + long num = pNumerator / gcd; + long den = pDenominator / gcd; + + numerator = pDenominator >= 0 ? num : -num; + denominator = pDenominator >= 0 ? den : -den; + } + + private static long gcd(final long m, final long n) { + if (m < 0) { + return gcd(n, -m); + } + + return n == 0 ? m : gcd(n, m % n); + } + + private static long lcm(final long m, final long n) { + if (m < 0) { + return lcm(n, -m); + } + + return m * (n / gcd(m, n)); // parentheses important to avoid overflow + } + + public long numerator() { + return numerator; + } + + public long denominator() { + return denominator; + } + + /// Number implementation + + @Override + public int intValue() { + return (int) doubleValue(); + } + + @Override + public long longValue() { + return (long) doubleValue(); + } + + @Override + public float floatValue() { + return (float) doubleValue(); + } + + @Override + public double doubleValue() { + if (this == NaN) { + return Double.NaN; + } + + return numerator / (double) denominator; + } + + /// Comparable implementation + + public int compareTo(final Rational pOther) { + double thisVal = doubleValue(); + double otherVal = pOther.doubleValue(); + + return thisVal < otherVal ? -1 : thisVal == otherVal ? 0 : 1; + } + + /// Object overrides + + @Override + public int hashCode() { + return Float.floatToIntBits(floatValue()); + } + + @Override + public boolean equals(final Object pOther) { + return pOther == this || pOther instanceof Rational && compareTo((Rational) pOther) == 0; + } + + @Override + public String toString() { + if (this == NaN) { + return "NaN"; + } + + return denominator == 1 ? Long.toString(numerator) : String.format("%s/%s", numerator, denominator); + } + + /// Operations (adapted from http://www.cs.princeton.edu/introcs/92symbolic/Rational.java.html) + // TODO: Naming! multiply/divide/add/subtract or times/divides/plus/minus + + // return a * b, staving off overflow as much as possible by cross-cancellation + public Rational times(final Rational pOther) { + // special cases + if (equals(ZERO) || pOther.equals(ZERO)) { + return ZERO; + } + + // reduce p1/q2 and p2/q1, then multiply, where a = p1/q1 and b = p2/q2 + Rational c = new Rational(numerator, pOther.denominator); + Rational d = new Rational(pOther.numerator, denominator); + + return new Rational(c.numerator * d.numerator, c.denominator * d.denominator); + } + + // return a + b, staving off overflow + public Rational plus(final Rational pOther) { + // special cases + if (equals(ZERO)) { + return pOther; + } + if (pOther.equals(ZERO)) { + return this; + } + + // Find gcd of numerators and denominators + long f = gcd(numerator, pOther.numerator); + long g = gcd(denominator, pOther.denominator); + + // add cross-product terms for numerator + // multiply back in + return new Rational( + ((numerator / f) * (pOther.denominator / g) + (pOther.numerator / f) * (denominator / g)) * f, + lcm(denominator, pOther.denominator) + ); + } + + // return -a + public Rational negate() { + return new Rational(-numerator, denominator); + } + + // return a - b + public Rational minus(final Rational pOther) { + return plus(pOther.negate()); + } + + public Rational reciprocal() { + return new Rational(denominator, numerator); + } + + // return a / b + public Rational divides(final Rational pOther) { + if (pOther.equals(ZERO)) { + throw new ArithmeticException("/ by zero"); + } + + return times(pOther.reciprocal()); + } +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFF.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFF.java new file mode 100644 index 00000000..539a2e12 --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFF.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2009, 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.tiff; + +/** + * TIFF + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TIFF.java,v 1.0 Nov 15, 2009 3:02:24 PM haraldk Exp$ + */ +@SuppressWarnings("UnusedDeclaration") +public interface TIFF { + short BYTE_ORDER_MARK_BIG_ENDIAN = ('M' << 8) | 'M'; + short BYTE_ORDER_MARK_LITTLE_ENDIAN = ('I' << 8) | 'I'; + + int TIFF_MAGIC = 42; + + short TYPE_BYTE = 1; + short TYPE_ASCII = 2; + short TYPE_SHORT = 3; + short TYPE_LONG = 4; + short TYPE_RATIONAL = 5; + + short TYPE_SBYTE = 6; + short TYPE_UNDEFINED = 7; + short TYPE_SSHORT = 8; + short TYPE_SLONG = 9; + short TYPE_SRATIONAL = 10; + short TYPE_FLOAT = 11; + short TYPE_DOUBLE = 12; + short TYPE_IFD = 13; + /* + 1 = BYTE 8-bit unsigned integer. + 2 = ASCII 8-bit byte that contains a 7-bit ASCII code; the last byte + must be NUL (binary zero). + 3 = SHORT 16-bit (2-byte) unsigned integer. + 4 = LONG 32-bit (4-byte) unsigned integer. + 5 = RATIONAL Two LONGs: the first represents the numerator of a + fraction; the second, the denominator. + + TIFF 6.0 and above: + 6 = SBYTE An 8-bit signed (twos-complement) integer. + 7 = UNDEFINED An 8-bit byte that may contain anything, depending on + the definition of the field. + 8 = SSHORT A 16-bit (2-byte) signed (twos-complement) integer. + 9 = SLONG A 32-bit (4-byte) signed (twos-complement) integer. + 10 = SRATIONAL Two SLONGs: the first represents the numerator of a + fraction, the second the denominator. + 11 = FLOAT Single precision (4-byte) IEEE format. + 12 = DOUBLE Double precision (8-byte) IEEE format. + + See http://www.awaresystems.be/imaging/tiff/tifftags/subifds.html + 13 = IFD, same as LONG + + TODO: BigTiff specifies more types + See http://www.awaresystems.be/imaging/tiff/bigtiff.html, http://www.remotesensing.org/libtiff/bigtiffdesign.html + (what about 14-15??) + 16 = TIFF_LONG8, being unsigned 8byte integer + 17 = TIFF_SLONG8, being signed 8byte integer + 18 = TIFF_IFD8, being a new unsigned 8byte IFD offset. + Should probably all map to Java long (and fail if high bit is set for the unsigned types???) + */ + String[] TYPE_NAMES = { + null, + "BYTE", "ASCII", "SHORT", "LONG", "RATIONAL", + "SBYTE", "UNDEFINED", "SSHORT", "SLONG", "SRATIONAL", "FLOAT", "DOUBLE", + "IFD", + null, null, + "LONG8", "SLONG8", "IFD8" + }; + /** Length of the corresponding type, in bytes. */ + int[] TYPE_LENGTHS = { + -1, + 1, 1, 2, 4, 8, + 1, 1, 2, 4, 8, 4, 8, + 4, + -1, -1, + 8, 8, 8 + }; + + /// EXIF defined TIFF tags + + int TAG_EXIF_IFD = 34665; + int TAG_GPS_IFD = 34853; + int TAG_INTEROP_IFD = 40965; + + /// A. Tags relating to image data structure: + + int TAG_IMAGE_WIDTH = 256; + int TAG_IMAGE_HEIGHT = 257; + int TAG_BITS_PER_SAMPLE = 258; + int TAG_COMPRESSION = 259; + int TAG_PHOTOMETRIC_INTERPRETATION = 262; + int TAG_FILL_ORDER = 266; + int TAG_ORIENTATION = 274; + int TAG_SAMPLES_PER_PIXEL = 277; + int TAG_PLANAR_CONFIGURATION = 284; + int TAG_SAMPLE_FORMAT = 339; + int TAG_YCBCR_SUB_SAMPLING = 530; + int TAG_YCBCR_POSITIONING = 531; + int TAG_X_RESOLUTION = 282; + int TAG_Y_RESOLUTION = 283; + int TAG_X_POSITION = 286; + int TAG_Y_POSITION = 287; + int TAG_RESOLUTION_UNIT = 296; + + /// B. Tags relating to recording offset + + int TAG_STRIP_OFFSETS = 273; + int TAG_ROWS_PER_STRIP = 278; + int TAG_STRIP_BYTE_COUNTS = 279; + int TAG_FREE_OFFSETS = 288; // "Not recommended for general interchange." + // "Old-style" JPEG (still used as EXIF thumbnail) + int TAG_JPEG_INTERCHANGE_FORMAT = 513; + int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514; + + int TAG_GROUP3OPTIONS = 292; + int TAG_GROUP4OPTIONS = 293; + + /// C. Tags relating to image data characteristics + + int TAG_TRANSFER_FUNCTION = 301; + int TAG_PREDICTOR = 317; + int TAG_WHITE_POINT = 318; + int TAG_PRIMARY_CHROMATICITIES = 319; + int TAG_COLOR_MAP = 320; + int TAG_INK_SET = 332; + int TAG_INK_NAMES = 333; + int TAG_NUMBER_OF_INKS = 334; + int TAG_EXTRA_SAMPLES = 338; + int TAG_TRANSFER_RANGE = 342; + int TAG_YCBCR_COEFFICIENTS = 529; + int TAG_REFERENCE_BLACK_WHITE = 532; + + /// D. Other tags + + int TAG_DATE_TIME = 306; + int TAG_DOCUMENT_NAME = 269; + int TAG_IMAGE_DESCRIPTION = 270; + int TAG_MAKE = 271; + int TAG_MODEL = 272; + int TAG_PAGE_NAME = 285; + int TAG_PAGE_NUMBER = 297; + int TAG_SOFTWARE = 305; + int TAG_ARTIST = 315; + int TAG_HOST_COMPUTER = 316; + int TAG_COPYRIGHT = 33432; + + int TAG_SUBFILE_TYPE = 254; + int TAG_OLD_SUBFILE_TYPE = 255; // Deprecated NO NOT WRITE! + int TAG_SUB_IFD = 330; + + /** + * XMP record. + * @see com.twelvemonkeys.imageio.metadata.xmp.XMP + */ + int TAG_XMP = 700; + + /** + * IPTC record. + * @see com.twelvemonkeys.imageio.metadata.iptc.IPTC + */ + int TAG_IPTC = 33723; + + /** + * Photoshop image resources. + * @see com.twelvemonkeys.imageio.metadata.psd.PSD + */ + int TAG_PHOTOSHOP = 34377; + + /** + * Photoshop layer and mask information (byte order follows TIFF container). + * Layer and mask information found in a typical layered Photoshop file. + * Starts with a character string of "Adobe Photoshop Document Data Block" + * (or "Adobe Photoshop Document Data V0002" for PSB) + * including the null termination character. + * @see com.twelvemonkeys.imageio.metadata.psd.PSD + */ + int TAG_PHOTOSHOP_IMAGE_SOURCE_DATA = 37724; + + int TAG_PHOTOSHOP_ANNOTATIONS = 50255; + + /** + * ICC Color Profile. + * @see java.awt.color.ICC_Profile + */ + int TAG_ICC_PROFILE = 34675; + + // Microsoft Office Document Imaging (MODI) + // http://msdn.microsoft.com/en-us/library/aa167596%28office.11%29.aspx + int TAG_MODI_BLC = 34718; + int TAG_MODI_VECTOR = 34719; + int TAG_MODI_PTC = 34720; + + // http://blogs.msdn.com/b/openspecification/archive/2009/12/08/details-of-three-tiff-tag-extensions-that-microsoft-office-document-imaging-modi-software-may-write-into-the-tiff-files-it-generates.aspx + int TAG_MODI_PLAIN_TEXT = 37679; + int TAG_MODI_OLE_PROPERTY_SET = 37680; + int TAG_MODI_TEXT_POS_INFO = 37681; + + int TAG_TILE_WIDTH = 322; + int TAG_TILE_HEIGTH = 323; + int TAG_TILE_OFFSETS = 324; + int TAG_TILE_BYTE_COUNTS = 325; + + // JPEG + int TAG_JPEG_TABLES = 347; + + // "Old-style" JPEG (Obsolete) DO NOT WRITE! + int TAG_OLD_JPEG_PROC = 512; + int TAG_OLD_JPEG_Q_TABLES = 519; + int TAG_OLD_JPEG_DC_TABLES = 520; + int TAG_OLD_JPEG_AC_TABLES = 521; +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFDirectory.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFDirectory.java similarity index 88% rename from imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFDirectory.java rename to imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFDirectory.java index 1ac2a027..4de0379f 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFDirectory.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFDirectory.java @@ -26,7 +26,7 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.twelvemonkeys.imageio.metadata.exif; +package com.twelvemonkeys.imageio.metadata.tiff; import com.twelvemonkeys.imageio.metadata.AbstractCompoundDirectory; import com.twelvemonkeys.imageio.metadata.Directory; @@ -34,14 +34,14 @@ import com.twelvemonkeys.imageio.metadata.Directory; import java.util.Collection; /** - * EXIFDirectory + * TIFFDirectory * * @author Harald Kuhr * @author last modified by $Author: haraldk$ - * @version $Id: EXIFDirectory.java,v 1.0 Nov 11, 2009 5:02:59 PM haraldk Exp$ + * @version $Id: TIFFDirectory.java,v 1.0 Nov 11, 2009 5:02:59 PM haraldk Exp$ */ -final class EXIFDirectory extends AbstractCompoundDirectory { - EXIFDirectory(final Collection directories) { +final class TIFFDirectory extends AbstractCompoundDirectory { + TIFFDirectory(final Collection directories) { super(directories); } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFEntry.java similarity index 75% rename from imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java rename to imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFEntry.java index 9d88ef24..9e71055f 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFEntry.java @@ -26,22 +26,54 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.twelvemonkeys.imageio.metadata.exif; +package com.twelvemonkeys.imageio.metadata.tiff; import com.twelvemonkeys.imageio.metadata.AbstractEntry; +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.exif.EXIF; +import com.twelvemonkeys.lang.Validate; + +import java.lang.reflect.Array; /** - * EXIFEntry + * Represents a TIFF IFD entry. * * @author Harald Kuhr * @author last modified by $Author: haraldk$ - * @version $Id: EXIFEntry.java,v 1.0 Nov 13, 2009 5:47:35 PM haraldk Exp$ + * @version $Id: TIFFEntry.java,v 1.0 Nov 13, 2009 5:47:35 PM haraldk Exp$ + * + * @see TIFF + * @see IFD */ -final class EXIFEntry extends AbstractEntry { - // TODO: Expose as TIFFEntry +public final class TIFFEntry extends AbstractEntry { final private short type; - EXIFEntry(final int identifier, final Object value, final short type) { + /** + * Creates a new {@code TIFFEntry}. + * + * @param identifier the TIFF tag identifier. + * @param value the value of the entry. + * + * @throws IllegalArgumentException if {@code value} is {@code null}. + * + * @see #TIFFEntry(int, short, Object) + */ + public TIFFEntry(final int identifier, final Object value) { + this(identifier, guessType(value), value); + } + + /** + * Creates a new {@code TIFFEntry}. + * + * @param identifier the TIFF tag identifier. + * @param type the type of the entry. + * @param value the value of the entry. + * + * @throws IllegalArgumentException if {@code type} is not a legal TIFF type. + * + * @see TIFF + */ + public TIFFEntry(final int identifier, final short type, final Object value) { super(identifier, value); if (type < 1 || type >= TIFF.TYPE_NAMES.length) { @@ -284,4 +316,82 @@ final class EXIFEntry extends AbstractEntry { public String getTypeName() { return TIFF.TYPE_NAMES[type]; } + + static short getType(final Entry entry) { + // For internal entries use type directly + if (entry instanceof TIFFEntry) { + TIFFEntry tiffEntry = (TIFFEntry) entry; + return tiffEntry.getType(); + } + + // For other entries, use name if it matches + Validate.notNull(entry, "entry"); + String typeName = entry.getTypeName(); + + if (typeName != null) { + for (int i = 1; i < TIFF.TYPE_NAMES.length; i++) { + if (typeName.equals(TIFF.TYPE_NAMES[i])) { + return (short) i; + } + } + } + + // Otherwise, fall back to guessing based on value's type + return guessType(entry.getValue()); + } + + private static short guessType(final Object entryValue) { + // Guess type based on native Java type + Object value = Validate.notNull(entryValue); + + 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 guessType not implemented for type %s", value.getClass())); + } + + static int getValueLength(final int pType, final int pCount) { + if (pType > 0 && pType < TIFF.TYPE_LENGTHS.length) { + return TIFF.TYPE_LENGTHS[pType] * pCount; + } + + return -1; + } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFReader.java new file mode 100644 index 00000000..cda9f231 --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFReader.java @@ -0,0 +1,572 @@ +/* + * Copyright (c) 2009, 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.tiff; + +import com.twelvemonkeys.imageio.metadata.CompoundDirectory; +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.MetadataReader; +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.lang.Validate; + +import javax.imageio.IIOException; +import javax.imageio.ImageIO; +import javax.imageio.stream.ImageInputStream; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.nio.ByteOrder; +import java.nio.charset.Charset; +import java.util.*; + +import static com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry.getValueLength; + +/** + * TIFFReader + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TIFFReader.java,v 1.0 Nov 13, 2009 5:42:51 PM haraldk Exp$ + */ +public final class TIFFReader extends MetadataReader { + + final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.metadata.exif.debug")); + + static final Collection KNOWN_IFDS = Collections.unmodifiableCollection(Arrays.asList(TIFF.TAG_EXIF_IFD, TIFF.TAG_GPS_IFD, TIFF.TAG_INTEROP_IFD, TIFF.TAG_SUB_IFD)); + + @Override + public Directory read(final ImageInputStream input) throws IOException { + Validate.notNull(input, "input"); + + byte[] bom = new byte[2]; + input.readFully(bom); + + if (bom[0] == 'I' && bom[1] == 'I') { + input.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } + else if (bom[0] == 'M' && bom[1] == 'M') { + input.setByteOrder(ByteOrder.BIG_ENDIAN); + } + else { + throw new IIOException(String.format("Invalid TIFF byte order mark '%s', expected: 'II' or 'MM'", StringUtil.decode(bom, 0, bom.length, "ASCII"))); + } + + // TODO: BigTiff uses version 43 instead of TIFF's 42, and header is slightly different, see + // http://www.awaresystems.be/imaging/tiff/bigtiff.html + int magic = input.readUnsignedShort(); + if (magic != TIFF.TIFF_MAGIC) { + throw new IIOException(String.format("Wrong TIFF magic in EXIF data: %04x, expected: %04x", magic, TIFF.TIFF_MAGIC)); + } + + long directoryOffset = input.readUnsignedInt(); + + return readDirectory(input, directoryOffset, true); + } + + // TODO: Consider re-writing so that the linked IFD parsing is done externally to the method + protected Directory readDirectory(final ImageInputStream pInput, final long pOffset, final boolean readLinked) throws IOException { + List ifds = new ArrayList<>(); + List entries = new ArrayList<>(); + + pInput.seek(pOffset); + long nextOffset = -1; + + int entryCount; + try { + entryCount = pInput.readUnsignedShort(); + } + catch (EOFException e) { + // Treat EOF here as empty Sub-IFD + entryCount = 0; + } + + for (int i = 0; i < entryCount; i++) { + try { + TIFFEntry entry = readEntry(pInput); + + if (entry != null) { + entries.add(entry); + } + } + catch (IIOException e) { + break; + } + } + + if (readLinked) { + if (nextOffset == -1) { + try { + nextOffset = pInput.readUnsignedInt(); + } + catch (EOFException e) { + // catch EOF here as missing EOF marker + nextOffset = 0; + } + } + + // Read linked IFDs + if (nextOffset != 0) { + CompoundDirectory next = (CompoundDirectory) readDirectory(pInput, nextOffset, true); + + for (int i = 0; i < next.directoryCount(); i++) { + ifds.add((IFD) next.getDirectory(i)); + } + } + } + + // TODO: Consider leaving to client code what sub-IFDs to parse (but always parse TAG_SUB_IFD). + readSubdirectories(pInput, entries, + Arrays.asList(TIFF.TAG_EXIF_IFD, TIFF.TAG_GPS_IFD, TIFF.TAG_INTEROP_IFD, TIFF.TAG_SUB_IFD) + ); + + ifds.add(0, new IFD(entries)); + + return new TIFFDirectory(ifds); + } + + // TODO: Might be better to leave this for client code, as it's tempting go really overboard and support any possible embedded format.. + private void readSubdirectories(ImageInputStream input, List entries, List subIFDIds) throws IOException { + if (subIFDIds == null || subIFDIds.isEmpty()) { + return; + } + + for (int i = 0, entriesSize = entries.size(); i < entriesSize; i++) { + TIFFEntry entry = (TIFFEntry) entries.get(i); + int tagId = (Integer) entry.getIdentifier(); + + if (subIFDIds.contains(tagId)) { + try { + if (KNOWN_IFDS.contains(tagId)) { + long[] pointerOffsets = getPointerOffsets(entry); + List subIFDs = new ArrayList<>(pointerOffsets.length); + + for (long pointerOffset : pointerOffsets) { + CompoundDirectory subDirectory = (CompoundDirectory) readDirectory(input, pointerOffset, false); + + for (int j = 0; j < subDirectory.directoryCount(); j++) { + subIFDs.add((IFD) subDirectory.getDirectory(j)); + } + } + + if (subIFDs.size() == 1) { + // Replace the entry with parsed data + entries.set(i, new TIFFEntry(tagId, entry.getType(), subIFDs.get(0))); + } + else { + // Replace the entry with parsed data + entries.set(i, new TIFFEntry(tagId, entry.getType(), subIFDs.toArray(new IFD[subIFDs.size()]))); + } + } + } + catch (IIOException e) { + if (DEBUG) { + // TODO: Issue warning without crashing...? + System.err.println("Error parsing sub-IFD: " + tagId); + e.printStackTrace(); + } + } + } + } + } + + private long[] getPointerOffsets(final Entry entry) throws IIOException { + long[] offsets; + Object value = entry.getValue(); + + if (value instanceof Byte) { + offsets = new long[] {(Byte) value & 0xff}; + } + else if (value instanceof Short) { + offsets = new long[] {(Short) value & 0xffff}; + } + else if (value instanceof Integer) { + offsets = new long[] {(Integer) value & 0xffffffffL}; + } + else if (value instanceof Long) { + offsets = new long[] {(Long) value}; + } + else if (value instanceof long[]) { + offsets = (long[]) value; + } + else { + throw new IIOException(String.format("Unknown pointer type: %s", (value != null + ? value.getClass() + : null))); + } + + return offsets; + } + + private TIFFEntry readEntry(final ImageInputStream pInput) throws IOException { + // TODO: BigTiff entries are different + int tagId = pInput.readUnsignedShort(); + short type = pInput.readShort(); + + int count = pInput.readInt(); // Number of values + + // It's probably a spec violation to have count 0, but we'll be lenient about it + if (count < 0) { + throw new IIOException(String.format("Illegal count %d for tag %s type %s @%08x", count, tagId, type, pInput.getStreamPosition())); + } + + if (type <= 0 || type > 13) { + pInput.skipBytes(4); // read Value + + // Invalid tag, this is just for debugging + long offset = pInput.getStreamPosition() - 12l; + + if (DEBUG) { + System.err.printf("Bad EXIF data @%08x\n", pInput.getStreamPosition()); + System.err.println("tagId: " + tagId + (tagId <= 0 ? " (INVALID)" : "")); + System.err.println("type: " + type + " (INVALID)"); + System.err.println("count: " + count); + + pInput.mark(); + pInput.seek(offset); + + try { + byte[] bytes = new byte[8 + Math.min(120, Math.max(24, count))]; + int len = pInput.read(bytes); + + if (DEBUG) { + System.err.print(HexDump.dump(offset, bytes, 0, len)); + System.err.println(len < count ? "[...]" : ""); + } + } + finally { + pInput.reset(); + } + } + return null; + } + + int valueLength = getValueLength(type, count); + + Object value; + // TODO: For BigTiff allow size > 4 && <= 8 in addition + if (valueLength > 0 && valueLength <= 4) { + value = readValueInLine(pInput, type, count); + pInput.skipBytes(4 - valueLength); + } + else { + long valueOffset = pInput.readUnsignedInt(); // This is the *value* iff the value size is <= 4 bytes + value = readValueAt(pInput, valueOffset, type, count); + } + + return new TIFFEntry(tagId, type, value); + } + + private Object readValueAt(final ImageInputStream pInput, final long pOffset, final short pType, final int pCount) throws IOException { + long pos = pInput.getStreamPosition(); + try { + pInput.seek(pOffset); + return readValue(pInput, pType, pCount); + } + catch (EOFException e) { + // TODO: Add warning listener API and report problem to client code + return e; + } + finally { + pInput.seek(pos); + } + } + + private Object readValueInLine(final ImageInputStream pInput, final short pType, final int pCount) throws IOException { + return readValue(pInput, pType, pCount); + } + + 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 TIFFEntry.getValue. + // TODO: Add getValueByte/getValueUnsignedByte/getValueShort/getValueUnsignedShort/getValueInt/etc... in API. + + long pos = pInput.getStreamPosition(); + + switch (pType) { + case TIFF.TYPE_ASCII: + // TODO: This might be UTF-8 or ISO-8859-x, even though spec says NULL-terminated 7 bit ASCII + // TODO: Fail if unknown chars, try parsing with ISO-8859-1 or file.encoding + if (pCount == 0) { + return ""; + } + byte[] ascii = new byte[pCount]; + pInput.readFully(ascii); + int len = ascii[ascii.length - 1] == 0 ? ascii.length - 1 : ascii.length; + return StringUtil.decode(ascii, 0, len, "UTF-8"); // UTF-8 is ASCII compatible + case TIFF.TYPE_BYTE: + if (pCount == 1) { + return pInput.readUnsignedByte(); + } + // else fall through + case TIFF.TYPE_SBYTE: + if (pCount == 1) { + return pInput.readByte(); + } + // else fall through + case TIFF.TYPE_UNDEFINED: + byte[] bytes = new byte[pCount]; + pInput.readFully(bytes); + + // NOTE: We don't change (unsigned) BYTE array wider Java type, as most often BYTE array means + // binary data and we want to keep that as a byte array for clients to parse further + + return bytes; + case TIFF.TYPE_SHORT: + if (pCount == 1) { + return pInput.readUnsignedShort(); + } + case TIFF.TYPE_SSHORT: + if (pCount == 1) { + return pInput.readShort(); + } + + short[] shorts = new short[pCount]; + pInput.readFully(shorts, 0, shorts.length); + + if (pType == TIFF.TYPE_SHORT) { + int[] ints = new int[pCount]; + for (int i = 0; i < pCount; i++) { + ints[i] = shorts[i] & 0xffff; + } + + return ints; + } + + return shorts; + case TIFF.TYPE_IFD: + case TIFF.TYPE_LONG: + if (pCount == 1) { + return pInput.readUnsignedInt(); + } + case TIFF.TYPE_SLONG: + if (pCount == 1) { + return pInput.readInt(); + } + + int[] ints = new int[pCount]; + pInput.readFully(ints, 0, ints.length); + + if (pType == TIFF.TYPE_LONG || pType == TIFF.TYPE_IFD) { + long[] longs = new long[pCount]; + for (int i = 0; i < pCount; i++) { + longs[i] = ints[i] & 0xffffffffL; + } + + return longs; + } + + return ints; + case TIFF.TYPE_FLOAT: + if (pCount == 1) { + return pInput.readFloat(); + } + + float[] floats = new float[pCount]; + pInput.readFully(floats, 0, floats.length); + return floats; + case TIFF.TYPE_DOUBLE: + if (pCount == 1) { + return pInput.readDouble(); + } + + double[] doubles = new double[pCount]; + pInput.readFully(doubles, 0, doubles.length); + return doubles; + + case TIFF.TYPE_RATIONAL: + if (pCount == 1) { + return createSafeRational(pInput.readUnsignedInt(), pInput.readUnsignedInt()); + } + + Rational[] rationals = new Rational[pCount]; + for (int i = 0; i < rationals.length; i++) { + rationals[i] = createSafeRational(pInput.readUnsignedInt(), pInput.readUnsignedInt()); + } + + return rationals; + case TIFF.TYPE_SRATIONAL: + if (pCount == 1) { + return createSafeRational(pInput.readInt(), pInput.readInt()); + } + + Rational[] srationals = new Rational[pCount]; + for (int i = 0; i < srationals.length; i++) { + srationals[i] = createSafeRational(pInput.readInt(), pInput.readInt()); + } + + return srationals; + + // BigTiff: + case 16: // LONG8 + case 17: // SLONG8 + case 18: // IFD8 + // TODO: Assert BigTiff (version == 43) + + if (pCount == 1) { + long val = pInput.readLong(); + if (pType != 17 && val < 0) { + throw new IIOException(String.format("Value > %s", Long.MAX_VALUE)); + } + return val; + } + + long[] longs = new long[pCount]; + for (int i = 0; i < pCount; i++) { + longs[i] = pInput.readLong(); + } + + return longs; + + default: + // Spec says skip unknown values + return new Unknown(pType, pCount, pos); + } + } + + private static Rational createSafeRational(final long numerator, final long denominator) throws IOException { + if (denominator == 0) { + // Bad data. + return Rational.NaN; + } + + return new Rational(numerator, denominator); + } + + public static void main(String[] args) throws IOException { + TIFFReader reader = new TIFFReader(); + ImageInputStream stream = ImageIO.createImageInputStream(new File(args[0])); + + long pos = 0; + if (args.length > 1) { + if (args[1].startsWith("0x")) { + pos = Integer.parseInt(args[1].substring(2), 16); + } + else { + pos = Long.parseLong(args[1]); + } + + stream.setByteOrder(pos < 0 ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN); + + pos = Math.abs(pos); + + stream.seek(pos); + } + + try { + Directory directory; + + if (args.length > 1) { + directory = reader.readDirectory(stream, pos, false); + } + else { + directory = reader.read(stream); + } + + for (Entry entry : directory) { + System.err.println(entry); + + Object value = entry.getValue(); + if (value instanceof byte[]) { + byte[] bytes = (byte[]) value; + System.err.println(HexDump.dump(0, bytes, 0, Math.min(bytes.length, 128))); + } + } + } + finally { + stream.close(); + } + } + + ////////////////////// + // TODO: Stream based hex dump util? + public static class HexDump { + private HexDump() { + } + + private static final int WIDTH = 32; + + public static String dump(byte[] bytes) { + return dump(0, bytes, 0, bytes.length); + } + + public static String dump(long offset, byte[] bytes, int off, int len) { + StringBuilder builder = new StringBuilder(); + + int i; + for (i = 0; i < len; i++) { + if (i % WIDTH == 0) { + if (i > 0) { + builder.append("\n"); + } + builder.append(String.format("%08x: ", i + off + offset)); + } + else if (i > 0 && i % 2 == 0) { + builder.append(" "); + } + + builder.append(String.format("%02x", bytes[i + off])); + + int next = i + 1; + if (next % WIDTH == 0 || next == len) { + int leftOver = (WIDTH - (next % WIDTH)) % WIDTH; + + if (leftOver != 0) { + // Pad: 5 spaces for every 2 bytes... Special care if padding is non-even. + int pad = leftOver / 2; + + if (len % 2 != 0) { + builder.append(" "); + } + + for (int j = 0; j < pad; j++) { + builder.append(" "); + } + } + + builder.append(" "); + builder.append(toAsciiString(bytes, next - (WIDTH - leftOver) + off, next + off)); + } + } + + return builder.toString(); + } + + private static String toAsciiString(final byte[] bytes, final int from, final int to) { + byte[] range = Arrays.copyOfRange(bytes, from, to); + + for (int i = 0; i < range.length; i++) { + if (range[i] < 32 || range[i] > 126) { + range[i] = '.'; // Unreadable char + } + } + + return new String(range, Charset.forName("ascii")); + } + } +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFWriter.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFWriter.java new file mode 100644 index 00000000..f814379d --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFWriter.java @@ -0,0 +1,414 @@ +/* + * 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.tiff; + +import com.twelvemonkeys.imageio.metadata.CompoundDirectory; +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.MetadataWriter; +import com.twelvemonkeys.lang.Validate; + +import javax.imageio.IIOException; +import javax.imageio.stream.ImageOutputStream; +import java.io.IOException; +import java.nio.ByteOrder; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.*; + +import static com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry.getType; +import static com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry.getValueLength; + +/** + * TIFFWriter + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TIFFWriter.java,v 1.0 17.07.13 10:20 haraldk Exp$ + */ +public final class TIFFWriter extends MetadataWriter { + + private static final int WORD_LENGTH = 2; + private static final int LONGWORD_LENGTH = 4; + private static final int ENTRY_LENGTH = 12; + + public boolean write(final Collection entries, final ImageOutputStream stream) throws IOException { + return write(new IFD(entries), stream); + } + + @Override + public boolean write(final Directory directory, final ImageOutputStream stream) throws IOException { + Validate.notNull(directory); + Validate.notNull(stream); + + // TODO: Should probably validate that the directory contains only valid TIFF entries... + // the writer will crash on non-Integer ids and unsupported types + // TODO: Implement the above validation in IFD constructor? + + writeTIFFHeader(stream); + + if (directory instanceof CompoundDirectory) { + CompoundDirectory compoundDirectory = (CompoundDirectory) directory; + + for (int i = 0; i < compoundDirectory.directoryCount(); i++) { + writeIFD(compoundDirectory.getDirectory(i), stream, false); + } + } + else { + writeIFD(directory, stream, false); + } + + // Offset to next IFD (EOF) + stream.writeInt(0); + + return true; + } + + public void writeTIFFHeader(final ImageOutputStream stream) throws IOException { + // Header + ByteOrder byteOrder = stream.getByteOrder(); + stream.writeShort(byteOrder == ByteOrder.BIG_ENDIAN ? TIFF.BYTE_ORDER_MARK_BIG_ENDIAN : TIFF.BYTE_ORDER_MARK_LITTLE_ENDIAN); + stream.writeShort(42); + } + + public long writeIFD(final Collection entries, final ImageOutputStream stream) throws IOException { + Validate.notNull(entries); + Validate.notNull(stream); + + return writeIFD(new IFD(entries), stream, false); + } + + private long writeIFD(final Directory original, final ImageOutputStream stream, final boolean isSubIFD) throws IOException { + // TIFF spec says tags should be in increasing order, enforce that when writing + Directory ordered = ensureOrderedDirectory(original); + + // Compute space needed for extra storage first, then write the offset to the IFD, so that the layout is: + // IFD offset + // + // IFD entries (values/offsets) + long dataOffset = stream.getStreamPosition(); + long dataSize = computeDataSize(ordered); + + // Offset to this IFD + final long ifdOffset = stream.getStreamPosition() + dataSize + LONGWORD_LENGTH; + + if (!isSubIFD) { + stream.writeInt(assertIntegerOffset(ifdOffset)); + dataOffset += LONGWORD_LENGTH; + + // Seek to offset + stream.seek(ifdOffset); + } + else { + dataOffset += WORD_LENGTH + ordered.size() * ENTRY_LENGTH; + } + + // Write directory + stream.writeShort(ordered.size()); + + for (Entry entry : ordered) { + // Write tag id + stream.writeShort((Integer) entry.getIdentifier()); + // Write tag type + stream.writeShort(getType(entry)); + // Write value count + stream.writeInt(getCount(entry)); + + // Write value + if (entry.getValue() instanceof Directory) { + // TODO: This could possibly be a compound directory, in which case the count should be > 1 + stream.writeInt(assertIntegerOffset(dataOffset)); + long streamPosition = stream.getStreamPosition(); + stream.seek(dataOffset); + Directory subIFD = (Directory) entry.getValue(); + writeIFD(subIFD, stream, true); + dataOffset += computeDataSize(subIFD); + stream.seek(streamPosition); + } + else { + dataOffset += writeValue(entry, dataOffset, stream); + } + } + + return ifdOffset; + } + + public long computeIFDSize(final Collection directory) { + return WORD_LENGTH + computeDataSize(new IFD(directory)) + directory.size() * ENTRY_LENGTH; + } + + public long computeIFDOffsetSize(final Collection directory) { + return computeDataSize(new IFD(directory)) + LONGWORD_LENGTH; + } + + private long computeDataSize(final Directory directory) { + long dataSize = 0; + + for (Entry entry : directory) { + int length = getValueLength(getType(entry), getCount(entry)); + + if (length < 0) { + throw new IllegalArgumentException(String.format("Unknown size for entry %s", entry)); + } + + if (length > LONGWORD_LENGTH) { + dataSize += length; + } + + if (entry.getValue() instanceof Directory) { + Directory subIFD = (Directory) entry.getValue(); + long subIFDSize = WORD_LENGTH + subIFD.size() * ENTRY_LENGTH + computeDataSize(subIFD); + dataSize += subIFDSize; + } + } + + return dataSize; + } + + private Directory ensureOrderedDirectory(final Directory directory) { + if (!isSorted(directory)) { + List entries = new ArrayList<>(directory.size()); + + for (Entry entry : directory) { + entries.add(entry); + } + + Collections.sort(entries, new Comparator() { + public int compare(Entry left, Entry right) { + return (Integer) left.getIdentifier() - (Integer) right.getIdentifier(); + } + }); + + return new IFD(entries); + } + + return directory; + } + + private boolean isSorted(final Directory directory) { + int lastTag = 0; + + for (Entry entry : directory) { + int tag = ((Integer) entry.getIdentifier()) & 0xffff; + + if (tag < lastTag) { + return false; + } + + lastTag = tag; + } + + return true; + } + + private long writeValue(final Entry entry, final long dataOffset, final ImageOutputStream stream) throws IOException { + short type = getType(entry); + int valueLength = 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(final Entry entry) { + Object value = entry.getValue(); + return value instanceof String ? ((String) value).getBytes(Charset.forName("UTF-8")).length + 1 : entry.valueCount(); + } + + private void writeValueInline(final Object value, final short type, final ImageOutputStream stream) throws IOException { + if (value.getClass().isArray()) { + switch (type) { + case TIFF.TYPE_UNDEFINED: + case TIFF.TYPE_BYTE: + case TIFF.TYPE_SBYTE: + stream.write((byte[]) value); + break; + + case TIFF.TYPE_SHORT: + case TIFF.TYPE_SSHORT: + 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: + case TIFF.TYPE_SLONG: + 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 LONG: " + value.getClass()); + } + + stream.writeInts(ints, 0, ints.length); + break; + + case TIFF.TYPE_RATIONAL: + case TIFF.TYPE_SRATIONAL: + Rational[] rationals = (Rational[]) value; + for (Rational rational : rationals) { + stream.writeInt((int) rational.numerator()); + stream.writeInt((int) rational.denominator()); + } + + break; + + case TIFF.TYPE_FLOAT: + float[] floats; + + if (value instanceof float[]) { + floats = (float[]) value; + } + else { + throw new IllegalArgumentException("Unsupported type for TIFF FLOAT: " + value.getClass()); + } + + stream.writeFloats(floats, 0, floats.length); + + break; + + case TIFF.TYPE_DOUBLE: + double[] doubles; + + if (value instanceof double[]) { + doubles = (double[]) value; + } + else { + throw new IllegalArgumentException("Unsupported type for TIFF FLOAT: " + value.getClass()); + } + + stream.writeDoubles(doubles, 0, doubles.length); + + break; + + default: + throw new IllegalArgumentException("Unsupported TIFF type: " + type); + } + } + else { + switch (type) { + case TIFF.TYPE_BYTE: + case TIFF.TYPE_SBYTE: + case TIFF.TYPE_UNDEFINED: + stream.writeByte(((Number) value).intValue()); + break; + case TIFF.TYPE_ASCII: + byte[] bytes = ((String) value).getBytes(StandardCharsets.UTF_8); + stream.write(bytes); + stream.write(0); + break; + case TIFF.TYPE_SHORT: + case TIFF.TYPE_SSHORT: + stream.writeShort(((Number) value).intValue()); + break; + case TIFF.TYPE_LONG: + case TIFF.TYPE_SLONG: + stream.writeInt(((Number) value).intValue()); + break; + case TIFF.TYPE_RATIONAL: + case TIFF.TYPE_SRATIONAL: + Rational rational = (Rational) value; + stream.writeInt((int) rational.numerator()); + stream.writeInt((int) rational.denominator()); + break; + case TIFF.TYPE_FLOAT: + stream.writeFloat(((Number) value).floatValue()); + break; + case TIFF.TYPE_DOUBLE: + stream.writeDouble(((Number) value).doubleValue()); + break; + + default: + throw new IllegalArgumentException("Unsupported TIFF type: " + type); + } + } + } + + private void writeValueAt(final long dataOffset, final Object value, final short type, final ImageOutputStream stream) throws IOException { + stream.writeInt(assertIntegerOffset(dataOffset)); + long position = stream.getStreamPosition(); + stream.seek(dataOffset); + writeValueInline(value, type, stream); + stream.seek(position); + } + + private int assertIntegerOffset(long offset) throws IIOException { + if (offset > Integer.MAX_VALUE - (long) Integer.MIN_VALUE) { + throw new IIOException("Integer overflow for TIFF stream"); + } + + return (int) offset; + } +} diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/Unknown.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/Unknown.java similarity index 96% rename from imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/Unknown.java rename to imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/Unknown.java index d8bbbf54..bb097d31 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/Unknown.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/Unknown.java @@ -1,4 +1,4 @@ -package com.twelvemonkeys.imageio.metadata.exif; +package com.twelvemonkeys.imageio.metadata.tiff; /** * Unknown diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReaderTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReaderTest.java index d8a82581..ee0f3e0e 100644 --- a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReaderTest.java +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReaderTest.java @@ -30,8 +30,8 @@ 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.imageio.metadata.MetadataReaderAbstractTest; +import com.twelvemonkeys.imageio.metadata.tiff.TIFF; import com.twelvemonkeys.imageio.stream.SubImageInputStream; import org.junit.Test; @@ -149,20 +149,6 @@ public class EXIFReaderTest extends MetadataReaderAbstractTest { assertEquals("", directory.getEntryById(TIFF.TAG_IMAGE_DESCRIPTION).getValue()); } - @Test - public void testReadBadDataRationalZeroDenominator() throws IOException { - // This image seems to contain bad Exif. But as other tools are able to read, so should we.. - ImageInputStream stream = ImageIO.createImageInputStream(getResource("/jpeg/exif-rgb-thumbnail-bad-exif-kodak-dc210.jpg")); - stream.seek(12); - Directory directory = createReader().read(new SubImageInputStream(stream, 21674)); - - // Special case: Rational with zero-denominator inside EXIF data - Directory exif = (Directory) directory.getEntryById(TIFF.TAG_EXIF_IFD).getValue(); - Entry entry = exif.getEntryById(EXIF.TAG_COMPRESSED_BITS_PER_PIXEL); - assertNotNull(entry); - assertEquals(Rational.NaN, entry.getValue()); - } - @Test public void testReadBadDirectoryCount() throws IOException { // This image seems to contain bad Exif. But as other tools are able to read, so should we.. diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriterTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriterTest.java index f682fd00..411f7010 100644 --- a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriterTest.java +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFWriterTest.java @@ -29,6 +29,10 @@ package com.twelvemonkeys.imageio.metadata.exif; import com.twelvemonkeys.imageio.metadata.*; +import com.twelvemonkeys.imageio.metadata.tiff.TIFF; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFWriter; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.io.FastByteArrayOutputStream; import org.junit.Test; @@ -41,18 +45,16 @@ import java.io.IOException; import java.io.InputStream; 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 + * TIFFWriterTest * * @author Harald Kuhr * @author last modified by $Author: haraldk$ - * @version $Id: EXIFWriterTest.java,v 1.0 18.07.13 09:53 haraldk Exp$ + * @version $Id: TIFFWriterTest.java,v 1.0 18.07.13 09:53 haraldk Exp$ */ public class EXIFWriterTest extends MetadataWriterAbstractTest { @@ -61,28 +63,28 @@ public class EXIFWriterTest extends MetadataWriterAbstractTest { return getResource("/exif/exif-jpeg-segment.bin").openStream(); } - protected EXIFReader createReader() { - return new EXIFReader(); + protected TIFFReader createReader() { + return new TIFFReader(); } @Override - protected EXIFWriter createWriter() { - return new EXIFWriter(); + protected TIFFWriter createWriter() { + return new TIFFWriter(); } @Test public void testWriteReadSimple() throws IOException { ArrayList entries = new ArrayList<>(); - entries.add(new EXIFEntry(TIFF.TAG_ORIENTATION, 1, TIFF.TYPE_SHORT)); - entries.add(new EXIFEntry(TIFF.TAG_IMAGE_WIDTH, 1600, TIFF.TYPE_SHORT)); + entries.add(new TIFFEntry(TIFF.TAG_ORIENTATION, TIFF.TYPE_SHORT, 1)); + entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, TIFF.TYPE_SHORT, 1600)); entries.add(new AbstractEntry(TIFF.TAG_IMAGE_HEIGHT, 900) {}); - entries.add(new EXIFEntry(TIFF.TAG_ARTIST, "Harald K.", TIFF.TYPE_ASCII)); + entries.add(new TIFFEntry(TIFF.TAG_ARTIST, TIFF.TYPE_ASCII, "Harald K.")); 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); + new TIFFWriter().write(directory, imageStream); imageStream.flush(); assertEquals(output.size(), imageStream.getStreamPosition()); @@ -95,13 +97,11 @@ public class EXIFWriterTest extends MetadataWriterAbstractTest { assertEquals(0, data[2]); assertEquals(42, data[3]); - Directory read = new EXIFReader().read(new ByteArrayImageInputStream(data)); + Directory read = new TIFFReader().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()); @@ -122,7 +122,7 @@ public class EXIFWriterTest extends MetadataWriterAbstractTest { public void testWriteMotorola() throws IOException { ArrayList entries = new ArrayList<>(); entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {}); - entries.add(new EXIFEntry(TIFF.TAG_IMAGE_WIDTH, Integer.MAX_VALUE, TIFF.TYPE_LONG)); + entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, TIFF.TYPE_LONG, Integer.MAX_VALUE)); Directory directory = new AbstractDirectory(entries) {}; ByteArrayOutputStream output = new FastByteArrayOutputStream(1024); @@ -130,7 +130,7 @@ public class EXIFWriterTest extends MetadataWriterAbstractTest { imageStream.setByteOrder(ByteOrder.BIG_ENDIAN); // BE = Motorola - new EXIFWriter().write(directory, imageStream); + new TIFFWriter().write(directory, imageStream); imageStream.flush(); assertEquals(output.size(), imageStream.getStreamPosition()); @@ -143,7 +143,7 @@ public class EXIFWriterTest extends MetadataWriterAbstractTest { assertEquals(0, data[2]); assertEquals(42, data[3]); - Directory read = new EXIFReader().read(new ByteArrayImageInputStream(data)); + Directory read = new TIFFReader().read(new ByteArrayImageInputStream(data)); assertNotNull(read); assertEquals(2, read.size()); @@ -157,7 +157,7 @@ public class EXIFWriterTest extends MetadataWriterAbstractTest { public void testWriteIntel() throws IOException { ArrayList entries = new ArrayList<>(); entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {}); - entries.add(new EXIFEntry(TIFF.TAG_IMAGE_WIDTH, Integer.MAX_VALUE, TIFF.TYPE_LONG)); + entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, TIFF.TYPE_LONG, Integer.MAX_VALUE)); Directory directory = new AbstractDirectory(entries) {}; ByteArrayOutputStream output = new FastByteArrayOutputStream(1024); @@ -165,7 +165,7 @@ public class EXIFWriterTest extends MetadataWriterAbstractTest { imageStream.setByteOrder(ByteOrder.LITTLE_ENDIAN); // LE = Intel - new EXIFWriter().write(directory, imageStream); + new TIFFWriter().write(directory, imageStream); imageStream.flush(); assertEquals(output.size(), imageStream.getStreamPosition()); @@ -178,7 +178,7 @@ public class EXIFWriterTest extends MetadataWriterAbstractTest { assertEquals(42, data[2]); assertEquals(0, data[3]); - Directory read = new EXIFReader().read(new ByteArrayImageInputStream(data)); + Directory read = new TIFFReader().read(new ByteArrayImageInputStream(data)); assertNotNull(read); assertEquals(2, read.size()); @@ -188,45 +188,15 @@ public class EXIFWriterTest extends MetadataWriterAbstractTest { assertEquals((long) Integer.MAX_VALUE, read.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue()); } - @Test - public void testNesting() throws IOException { - EXIFEntry artist = new EXIFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO", TIFF.TYPE_ASCII); - - EXIFEntry subSubSubSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(artist)), TIFF.TYPE_LONG); - EXIFEntry subSubSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubSubSubIFD)), TIFF.TYPE_LONG); - EXIFEntry subSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubSubIFD)), TIFF.TYPE_LONG); - EXIFEntry subIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubIFD)), TIFF.TYPE_LONG); - - Directory directory = new IFD(Collections.singletonList(subIFD)); - - ByteArrayOutputStream output = new FastByteArrayOutputStream(1024); - ImageOutputStream imageStream = ImageIO.createImageOutputStream(output); - - new EXIFWriter().write(directory, imageStream); - imageStream.flush(); - - assertEquals(output.size(), imageStream.getStreamPosition()); - - Directory read = new EXIFReader().read(new ByteArrayImageInputStream(output.toByteArray())); - - assertNotNull(read); - assertEquals(1, read.size()); - assertEquals(subIFD, read.getEntryById(TIFF.TAG_SUB_IFD)); // Recursively tests content! - } - @Test public void testReadWriteRead() throws IOException { Directory original = createReader().read(getDataAsIIS()); ByteArrayOutputStream output = new FastByteArrayOutputStream(256); - ImageOutputStream imageOutput = ImageIO.createImageOutputStream(output); - try { + try (ImageOutputStream imageOutput = ImageIO.createImageOutputStream(output)) { createWriter().write(original, imageOutput); } - finally { - imageOutput.close(); - } Directory read = createReader().read(new ByteArrayImageInputStream(output.toByteArray())); @@ -236,35 +206,16 @@ public class EXIFWriterTest extends MetadataWriterAbstractTest { @Test public void testComputeIFDSize() throws IOException { ArrayList entries = new ArrayList<>(); - entries.add(new EXIFEntry(TIFF.TAG_ORIENTATION, 1, TIFF.TYPE_SHORT)); - entries.add(new EXIFEntry(TIFF.TAG_IMAGE_WIDTH, 1600, TIFF.TYPE_SHORT)); + entries.add(new TIFFEntry(TIFF.TAG_ORIENTATION, TIFF.TYPE_SHORT, 1)); + entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, TIFF.TYPE_SHORT, 1600)); entries.add(new AbstractEntry(TIFF.TAG_IMAGE_HEIGHT, 900) {}); - entries.add(new EXIFEntry(TIFF.TAG_ARTIST, "Harald K.", TIFF.TYPE_ASCII)); + entries.add(new TIFFEntry(TIFF.TAG_ARTIST, TIFF.TYPE_ASCII, "Harald K.")); entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {}); - EXIFWriter writer = createWriter(); + TIFFWriter writer = createWriter(); ImageOutputStream stream = new NullImageOutputStream(); - writer.write(new IFD(entries), stream); - - assertEquals(stream.getStreamPosition(), writer.computeIFDSize(entries) + 12); - } - - @Test - public void testComputeIFDSizeNested() throws IOException { - EXIFEntry artist = new EXIFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO", TIFF.TYPE_ASCII); - - EXIFEntry subSubSubSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(artist)), TIFF.TYPE_LONG); - EXIFEntry subSubSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubSubSubIFD)), TIFF.TYPE_LONG); - EXIFEntry subSubIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubSubIFD)), TIFF.TYPE_LONG); - EXIFEntry subIFD = new EXIFEntry(TIFF.TAG_SUB_IFD, new IFD(Collections.singletonList(subSubIFD)), TIFF.TYPE_LONG); - - List entries = Collections.singletonList(subIFD); - - EXIFWriter writer = createWriter(); - - ImageOutputStream stream = new NullImageOutputStream(); - writer.write(new IFD(entries), stream); + writer.write(entries, stream); assertEquals(stream.getStreamPosition(), writer.computeIFDSize(entries) + 12); } diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/RationalTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/RationalTest.java new file mode 100644 index 00000000..9c319c4d --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/RationalTest.java @@ -0,0 +1,54 @@ +package com.twelvemonkeys.imageio.metadata.exif; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * RationalTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: RationalTest.java,v 1.0 Nov 18, 2009 3:23:17 PM haraldk Exp$ + */ +@SuppressWarnings("deprecation") +public class RationalTest { + @Test(expected = IllegalArgumentException.class) + public void testZeroDenominator() { + new Rational(1, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testLongMinValueNumerator() { + new Rational(Long.MIN_VALUE, 1); + } + + @Test(expected = IllegalArgumentException.class) + public void testLongMinValueDenominator() { + new Rational(1, Long.MIN_VALUE); + } + + @Test + public void testEquals() { + assertEquals(new Rational(0, 1), new Rational(0, 999)); + assertEquals(new Rational(0, 1), new Rational(0, -1)); + assertEquals(new Rational(1, 2), new Rational(1000000, 2000000)); + assertEquals(new Rational(1, -2), new Rational(-1, 2)); + + Rational x = new Rational(1, -2); + Rational y = new Rational(-1000000, 2000000); + assertEquals(x, y); + assertEquals(x.numerator(), y.numerator()); + assertEquals(x.denominator(), y.denominator()); + } + + @Test + public void testEqualsBoundaries() { + assertEquals(new Rational(Long.MAX_VALUE, Long.MAX_VALUE), new Rational(1, 1)); + + // NOTE: Math.abs(Long.MIN_VALUE) == Long.MIN_VALUE... :-P + assertEquals(new Rational(Long.MIN_VALUE + 1, Long.MIN_VALUE + 1), new Rational(1, 1)); + assertEquals(new Rational(Long.MIN_VALUE + 1, Long.MAX_VALUE), new Rational(-1, 1)); + assertEquals(new Rational(Long.MAX_VALUE, Long.MIN_VALUE + 1), new Rational(-1, 1)); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/IFDTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/IFDTest.java similarity index 97% rename from imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/IFDTest.java rename to imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/IFDTest.java index 6d49f46a..e7fcf131 100644 --- a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/IFDTest.java +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/IFDTest.java @@ -26,7 +26,7 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.twelvemonkeys.imageio.metadata.exif; +package com.twelvemonkeys.imageio.metadata.tiff; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.DirectoryAbstractTest; diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/RationalTestCase.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/RationalTest.java similarity index 79% rename from imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/RationalTestCase.java rename to imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/RationalTest.java index 819c334e..9767a9f4 100644 --- a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/RationalTestCase.java +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/RationalTest.java @@ -1,41 +1,34 @@ -package com.twelvemonkeys.imageio.metadata.exif; +package com.twelvemonkeys.imageio.metadata.tiff; -import junit.framework.TestCase; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; /** - * RationalTestCase + * RationalTest * * @author Harald Kuhr * @author last modified by $Author: haraldk$ - * @version $Id: RationalTestCase.java,v 1.0 Nov 18, 2009 3:23:17 PM haraldk Exp$ + * @version $Id: RationalTest.java,v 1.0 Nov 18, 2009 3:23:17 PM haraldk Exp$ */ -public class RationalTestCase extends TestCase { +public class RationalTest { + @Test(expected = IllegalArgumentException.class) public void testZeroDenominator() { - try { - new Rational(1, 0); - fail("IllegalArgumentException expected"); - } - catch (IllegalArgumentException expected) { - } + new Rational(1, 0); } // TODO: Find a solution to this problem, as we should be able to work with it... - public void testLongMinValue() { - try { - new Rational(Long.MIN_VALUE, 1); - fail("IllegalArgumentException expected"); - } - catch (IllegalArgumentException expected) { - } - - try { - new Rational(1, Long.MIN_VALUE); - fail("IllegalArgumentException expected"); - } - catch (IllegalArgumentException expected) { - } + @Test(expected = IllegalArgumentException.class) + public void testLongMinValueNumerator() { + new Rational(Long.MIN_VALUE, 1); } + @Test(expected = IllegalArgumentException.class) + public void testLongMinValueDenominator() { + new Rational(1, Long.MIN_VALUE); + } + + @Test public void testEquals() { assertEquals(new Rational(0, 1), new Rational(0, 999)); assertEquals(new Rational(0, 1), new Rational(0, -1)); @@ -47,9 +40,9 @@ public class RationalTestCase extends TestCase { assertEquals(x, y); assertEquals(x.numerator(), y.numerator()); assertEquals(x.denominator(), y.denominator()); - } + @Test public void testEqualsBoundaries() { assertEquals(new Rational(Long.MAX_VALUE, Long.MAX_VALUE), new Rational(1, 1)); @@ -59,16 +52,19 @@ public class RationalTestCase extends TestCase { assertEquals(new Rational(Long.MAX_VALUE, Long.MIN_VALUE + 1), new Rational(-1, 1)); } + @Test public void testReciprocal() { assertEquals(new Rational(1, 99), new Rational(99, 1).reciprocal()); assertEquals(new Rational(-1, 1234567), new Rational(-1234567, 1).reciprocal()); } + @Test public void testNegate() { assertEquals(new Rational(-1, 99), new Rational(1, 99).negate()); assertEquals(new Rational(1, 1234567), new Rational(1, -1234567).negate()); } + @Test public void testPlus() { Rational x, y; @@ -96,6 +92,7 @@ public class RationalTestCase extends TestCase { assertEquals(x, x.plus(Rational.ZERO)); } + @Test public void testTimes() { Rational x, y; @@ -113,6 +110,7 @@ public class RationalTestCase extends TestCase { assertEquals(Rational.ZERO, x.times(Rational.ZERO)); } + @Test public void testMinus() { // 1/6 - -4/-8 = -1/3 Rational x = new Rational(1, 6); @@ -123,6 +121,7 @@ public class RationalTestCase extends TestCase { assertEquals(x, x.minus(Rational.ZERO)); } + @Test public void testDivides() { // 3037141/3247033 / 3246599/3037547 = 841/961 Rational x = new Rational(3037141, 3247033); @@ -133,11 +132,8 @@ public class RationalTestCase extends TestCase { assertEquals(Rational.ZERO, new Rational(0, 386).divides(x)); } + @Test(expected = ArithmeticException.class) public void testDivideZero() { - try { - new Rational(3037141, 3247033).divides(new Rational(0, 1)); - } - catch (ArithmeticException expected) { - } + new Rational(3037141, 3247033).divides(new Rational(0, 1)); } } diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFDirectoryTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFDirectoryTest.java similarity index 89% rename from imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFDirectoryTest.java rename to imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFDirectoryTest.java index bc583c44..fad5355a 100644 --- a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFDirectoryTest.java +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFDirectoryTest.java @@ -26,7 +26,7 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.twelvemonkeys.imageio.metadata.exif; +package com.twelvemonkeys.imageio.metadata.tiff; import com.twelvemonkeys.imageio.metadata.CompoundDirectory; import com.twelvemonkeys.imageio.metadata.CompoundDirectoryAbstractTest; @@ -35,15 +35,15 @@ import com.twelvemonkeys.imageio.metadata.Directory; import java.util.Collection; /** - * EXIFDirectoryTest + * TIFFDirectoryTest * * @author Harald Kuhr * @author last modified by $Author: haraldk$ - * @version $Id: EXIFDirectoryTest.java,v 1.0 02.01.12 16:41 haraldk Exp$ + * @version $Id: TIFFDirectoryTest.java,v 1.0 02.01.12 16:41 haraldk Exp$ */ -public class EXIFDirectoryTest extends CompoundDirectoryAbstractTest { +public class TIFFDirectoryTest extends CompoundDirectoryAbstractTest { @Override protected CompoundDirectory createCompoundDirectory(Collection directories) { - return new EXIFDirectory(directories); + return new TIFFDirectory(directories); } } diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntryTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFEntryTest.java similarity index 87% rename from imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntryTest.java rename to imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFEntryTest.java index bc34b811..6e83d7e9 100644 --- a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntryTest.java +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFEntryTest.java @@ -26,27 +26,27 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.twelvemonkeys.imageio.metadata.exif; +package com.twelvemonkeys.imageio.metadata.tiff; import com.twelvemonkeys.imageio.metadata.Entry; import com.twelvemonkeys.imageio.metadata.EntryAbstractTest; import org.junit.Test; /** - * EXIFEntryTest + * TIFFEntryTest * * @author Harald Kuhr * @author last modified by $Author: haraldk$ - * @version $Id: EXIFEntryTest.java,v 1.0 02.01.12 17:35 haraldk Exp$ + * @version $Id: TIFFEntryTest.java,v 1.0 02.01.12 17:35 haraldk Exp$ */ -public class EXIFEntryTest extends EntryAbstractTest { +public class TIFFEntryTest extends EntryAbstractTest { @Override protected Entry createEntry(final Object value) { return createEXIFEntry(TIFF.TAG_COPYRIGHT, value, (short) 2); } - private EXIFEntry createEXIFEntry(final int identifier, final Object value, final int type) { - return new EXIFEntry(identifier, value, (short) type); + private TIFFEntry createEXIFEntry(final int identifier, final Object value, final int type) { + return new TIFFEntry(identifier, (short) type, value); } @Test(expected = IllegalArgumentException.class) diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFReaderTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFReaderTest.java new file mode 100644 index 00000000..eeb7f36c --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFReaderTest.java @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2011, 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.tiff; + +import com.twelvemonkeys.imageio.metadata.CompoundDirectory; +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.Entry; +import com.twelvemonkeys.imageio.metadata.MetadataReaderAbstractTest; +import com.twelvemonkeys.imageio.metadata.exif.EXIF; +import com.twelvemonkeys.imageio.stream.SubImageInputStream; +import org.junit.Test; + +import javax.imageio.ImageIO; +import javax.imageio.stream.ImageInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.*; + +/** + * TIFFReaderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TIFFReaderTest.java,v 1.0 23.12.11 13:50 haraldk Exp$ + */ +public class TIFFReaderTest extends MetadataReaderAbstractTest { + @Override + protected InputStream getData() throws IOException { + return getResource("/exif/exif-jpeg-segment.bin").openStream(); + } + + @Override + protected TIFFReader createReader() { + return new TIFFReader(); + } + + @Test + public void testIsCompoundDirectory() throws IOException { + Directory exif = createReader().read(getDataAsIIS()); + assertThat(exif, instanceOf(CompoundDirectory.class)); + } + + @Test + public void testDirectory() throws IOException { + CompoundDirectory exif = (CompoundDirectory) createReader().read(getDataAsIIS()); + + assertEquals(2, exif.directoryCount()); + assertNotNull(exif.getDirectory(0)); + assertNotNull(exif.getDirectory(1)); + assertEquals(exif.size(), exif.getDirectory(0).size() + exif.getDirectory(1).size()); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testDirectoryOutOfBounds() throws IOException { + InputStream data = getData(); + + CompoundDirectory exif = (CompoundDirectory) createReader().read(ImageIO.createImageInputStream(data)); + + assertEquals(2, exif.directoryCount()); + assertNotNull(exif.getDirectory(exif.directoryCount())); + } + + @Test + public void testEntries() throws IOException { + CompoundDirectory exif = (CompoundDirectory) createReader().read(getDataAsIIS()); + + // From IFD0 + assertNotNull(exif.getEntryById(TIFF.TAG_SOFTWARE)); + assertEquals("Adobe Photoshop CS2 Macintosh", exif.getEntryById(TIFF.TAG_SOFTWARE).getValue()); + assertEquals(exif.getEntryById(TIFF.TAG_SOFTWARE), exif.getEntryByFieldName("Software")); + + // From IFD1 + assertNotNull(exif.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT)); + assertEquals((long) 418, exif.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT).getValue()); + assertEquals(exif.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT), exif.getEntryByFieldName("JPEGInterchangeFormat")); + } + + @Test + public void testIFD0() throws IOException { + CompoundDirectory exif = (CompoundDirectory) createReader().read(getDataAsIIS()); + + Directory ifd0 = exif.getDirectory(0); + assertNotNull(ifd0); + + assertNotNull(ifd0.getEntryById(TIFF.TAG_IMAGE_WIDTH)); + assertEquals(3601, ifd0.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue()); + + assertNotNull(ifd0.getEntryById(TIFF.TAG_IMAGE_HEIGHT)); + assertEquals(4176, ifd0.getEntryById(TIFF.TAG_IMAGE_HEIGHT).getValue()); + + // Assert 'uncompressed' (there's no TIFF image here, really) + assertNotNull(ifd0.getEntryById(TIFF.TAG_COMPRESSION)); + assertEquals(1, ifd0.getEntryById(TIFF.TAG_COMPRESSION).getValue()); + } + + @Test + public void testIFD1() throws IOException { + CompoundDirectory exif = (CompoundDirectory) createReader().read(getDataAsIIS()); + + Directory ifd1 = exif.getDirectory(1); + assertNotNull(ifd1); + + // Assert 'JPEG compression' (thumbnail only) + assertNotNull(ifd1.getEntryById(TIFF.TAG_COMPRESSION)); + assertEquals(6, ifd1.getEntryById(TIFF.TAG_COMPRESSION).getValue()); + + assertNull(ifd1.getEntryById(TIFF.TAG_IMAGE_WIDTH)); + assertNull(ifd1.getEntryById(TIFF.TAG_IMAGE_HEIGHT)); + } + + @Test + public void testReadBadDataZeroCount() throws IOException { + // This image seems to contain bad Exif. But as other tools are able to read, so should we.. + ImageInputStream stream = ImageIO.createImageInputStream(getResource("/jpeg/exif-rgb-thumbnail-bad-exif-kodak-dc210.jpg")); + stream.seek(12); + Directory directory = createReader().read(new SubImageInputStream(stream, 21674)); + + assertEquals(22, directory.size()); + + // Special case: Ascii string with count == 0, not ok according to spec (?), but we'll let it pass + assertEquals("", directory.getEntryById(TIFF.TAG_IMAGE_DESCRIPTION).getValue()); + } + + @Test + public void testReadBadDataRationalZeroDenominator() throws IOException { + // This image seems to contain bad Exif. But as other tools are able to read, so should we.. + ImageInputStream stream = ImageIO.createImageInputStream(getResource("/jpeg/exif-rgb-thumbnail-bad-exif-kodak-dc210.jpg")); + stream.seek(12); + Directory directory = createReader().read(new SubImageInputStream(stream, 21674)); + + // Special case: Rational with zero-denominator inside EXIF data + Directory exif = (Directory) directory.getEntryById(TIFF.TAG_EXIF_IFD).getValue(); + Entry entry = exif.getEntryById(EXIF.TAG_COMPRESSED_BITS_PER_PIXEL); + assertNotNull(entry); + assertEquals(Rational.NaN, entry.getValue()); + } + + @Test + public void testReadBadDirectoryCount() throws IOException { + // This image seems to contain bad Exif. But as other tools are able to read, so should we.. + ImageInputStream stream = ImageIO.createImageInputStream(getResource("/jpeg/exif-bad-directory-entry-count.jpg")); + stream.seek(4424 + 10); + + Directory directory = createReader().read(new SubImageInputStream(stream, 214 - 6)); + assertEquals(7, directory.size()); // TIFF structure says 8, but the last entry isn't there + + Directory exif = (Directory) directory.getEntryById(TIFF.TAG_EXIF_IFD).getValue(); + assertNotNull(exif); + assertEquals(3, exif.size()); + } + + @Test + public void testTIFFWithBadExifIFD() throws IOException { + // This image seems to contain bad TIFF data. But as other tools are able to read, so should we.. + // It seems that the EXIF data (at offset 494196 or 0x78a74) overlaps with a custom + // Microsoft 'OLE Property Set' entry at 0x78a70 (UNDEFINED, count 5632)... + ImageInputStream stream = ImageIO.createImageInputStream(getResource("/tiff/chifley_logo.tif")); + Directory directory = createReader().read(stream); + assertEquals(22, directory.size()); + + // Some (all?) of the EXIF data is duplicated in the XMP, meaning PhotoShop can probably re-create it + Directory exif = (Directory) directory.getEntryById(TIFF.TAG_EXIF_IFD).getValue(); + assertNotNull(exif); + assertEquals(0, exif.size()); // EXIFTool reports "Warning: Bad ExifIFD directory" + } + + @Test + public void testReadExifJPEGWithInteropSubDirR98() throws IOException { + ImageInputStream stream = ImageIO.createImageInputStream(getResource("/jpeg/exif-with-interop-subdir-R98.jpg")); + stream.seek(30); + + CompoundDirectory directory = (CompoundDirectory) createReader().read(new SubImageInputStream(stream, 1360)); + assertEquals(17, directory.size()); + assertEquals(2, directory.directoryCount()); + + Directory exif = (Directory) directory.getEntryById(TIFF.TAG_EXIF_IFD).getValue(); + assertNotNull(exif); + assertEquals(23, exif.size()); + + // The interop IFD is empty (entry count is 0) + Directory interop = (Directory) exif.getEntryById(TIFF.TAG_INTEROP_IFD).getValue(); + assertNotNull(interop); + assertEquals(2, interop.size()); + + assertNotNull(interop.getEntryById(1)); // InteropIndex + assertEquals("ASCII", interop.getEntryById(1).getTypeName()); + assertEquals("R98", interop.getEntryById(1).getValue()); // Known values: R98, THM or R03 + + assertNotNull(interop.getEntryById(2)); // InteropVersion + assertEquals("UNDEFINED", interop.getEntryById(2).getTypeName()); + assertArrayEquals(new byte[] {'0', '1', '0', '0'}, (byte[]) interop.getEntryById(2).getValue()); + } + + @Test + public void testReadExifJPEGWithInteropSubDirEmpty() throws IOException { + ImageInputStream stream = ImageIO.createImageInputStream(getResource("/jpeg/exif-with-interop-subdir-empty.jpg")); + stream.seek(30); + + CompoundDirectory directory = (CompoundDirectory) createReader().read(new SubImageInputStream(stream, 1360)); + assertEquals(11, directory.size()); + assertEquals(1, directory.directoryCount()); + + Directory exif = (Directory) directory.getEntryById(TIFF.TAG_EXIF_IFD).getValue(); + assertNotNull(exif); + assertEquals(24, exif.size()); + + // The interop IFD is empty (entry count is 0) + Directory interop = (Directory) exif.getEntryById(TIFF.TAG_INTEROP_IFD).getValue(); + assertNotNull(interop); + assertEquals(0, interop.size()); + } + + @Test + public void testReadExifJPEGWithInteropSubDirEOF() throws IOException { + ImageInputStream stream = ImageIO.createImageInputStream(getResource("/jpeg/exif-with-interop-subdir-eof.jpg")); + stream.seek(30); + + CompoundDirectory directory = (CompoundDirectory) createReader().read(new SubImageInputStream(stream, 236)); + assertEquals(8, directory.size()); + assertEquals(1, directory.directoryCount()); + + Directory exif = (Directory) directory.getEntryById(TIFF.TAG_EXIF_IFD).getValue(); + assertNotNull(exif); + assertEquals(5, exif.size()); + + // The interop IFD isn't there (offset points to outside the TIFF structure)... + // Have double-checked using ExifTool, which says "Warning : Bad InteropOffset SubDirectory start" + Directory interop = (Directory) exif.getEntryById(TIFF.TAG_INTEROP_IFD).getValue(); + assertNotNull(interop); + assertEquals(0, interop.size()); + } + + @Test + public void testReadExifJPEGWithInteropSubDirBad() throws IOException { + ImageInputStream stream = ImageIO.createImageInputStream(getResource("/jpeg/exif-with-interop-subdir-bad.jpg")); + stream.seek(30); + + CompoundDirectory directory = (CompoundDirectory) createReader().read(new SubImageInputStream(stream, 12185)); + assertEquals(16, directory.size()); + assertEquals(2, directory.directoryCount()); + + Directory exif = (Directory) directory.getEntryById(TIFF.TAG_EXIF_IFD).getValue(); + assertNotNull(exif); + assertEquals(26, exif.size()); + + // JPEG starts at offset 1666 and length 10519, interop IFD points to offset 1900... + // Have double-checked using ExifTool, which says "Warning : Bad InteropIFD directory" + Directory interop = (Directory) exif.getEntryById(TIFF.TAG_INTEROP_IFD).getValue(); + assertNotNull(interop); + assertEquals(0, interop.size()); + } + + @Test + public void testReadExifWithMissingEOFMarker() throws IOException { + try (ImageInputStream stream = ImageIO.createImageInputStream(getResource("/exif/noeof.tif"))) { + CompoundDirectory directory = (CompoundDirectory) createReader().read(stream); + assertEquals(15, directory.size()); + assertEquals(1, directory.directoryCount()); + } + } + + @Test + public void testReadExifWithEmptyTag() throws IOException { + try (ImageInputStream stream = ImageIO.createImageInputStream(getResource("/exif/emptyexiftag.tif"))) { + CompoundDirectory directory = (CompoundDirectory) createReader().read(stream); + assertEquals(3, directory.directoryCount()); + } + } + + @Test + public void testReadValueBeyondEOF() throws IOException { + try (ImageInputStream stream = ImageIO.createImageInputStream(getResource("/exif/value-beyond-eof.tif"))) { + CompoundDirectory directory = (CompoundDirectory) createReader().read(stream); + assertEquals(1, directory.directoryCount()); + assertEquals(5, directory.size()); + + assertEquals(1, directory.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION).getValue()); + assertEquals(10, directory.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue()); + assertEquals(10, directory.getEntryById(TIFF.TAG_IMAGE_HEIGHT).getValue()); + assertEquals(42L, directory.getEntryById(32935).getValue()); + // NOTE: Assumes current implementation, could possibly change in the future. + assertTrue(directory.getEntryById(32934).getValue() instanceof EOFException); + } + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFWriterTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFWriterTest.java new file mode 100644 index 00000000..d017e4a6 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFWriterTest.java @@ -0,0 +1,293 @@ +/* + * 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.tiff; + +import com.twelvemonkeys.imageio.metadata.*; +import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; +import com.twelvemonkeys.io.FastByteArrayOutputStream; +import org.junit.Test; + +import javax.imageio.ImageIO; +import javax.imageio.stream.ImageOutputStream; +import javax.imageio.stream.ImageOutputStreamImpl; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +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; + +/** + * TIFFWriterTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TIFFWriterTest.java,v 1.0 18.07.13 09:53 haraldk Exp$ + */ +public class TIFFWriterTest extends MetadataWriterAbstractTest { + + @Override + protected InputStream getData() throws IOException { + return getResource("/exif/exif-jpeg-segment.bin").openStream(); + } + + protected TIFFReader createReader() { + return new TIFFReader(); + } + + @Override + protected TIFFWriter createWriter() { + return new TIFFWriter(); + } + + @Test + public void testWriteReadSimple() throws IOException { + ArrayList entries = new ArrayList<>(); + entries.add(new TIFFEntry(TIFF.TAG_ORIENTATION, TIFF.TYPE_SHORT, 1)); + entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, TIFF.TYPE_SHORT, 1600)); + entries.add(new AbstractEntry(TIFF.TAG_IMAGE_HEIGHT, 900) {}); + entries.add(new TIFFEntry(TIFF.TAG_ARTIST, TIFF.TYPE_ASCII, "Harald K.")); + 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 TIFFWriter().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 TIFFReader().read(new ByteArrayImageInputStream(data)); + + assertNotNull(read); + assertEquals(5, read.size()); + + // TODO: Assert that the tags are written in ascending order (don't test the read directory, but the file structure)! + + assertNotNull(read.getEntryById(TIFF.TAG_SOFTWARE)); + assertEquals("TwelveMonkeys ImageIO", read.getEntryById(TIFF.TAG_SOFTWARE).getValue()); + + assertNotNull(read.getEntryById(TIFF.TAG_IMAGE_WIDTH)); + assertEquals(1600, read.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue()); + + assertNotNull(read.getEntryById(TIFF.TAG_IMAGE_HEIGHT)); + assertEquals(900, read.getEntryById(TIFF.TAG_IMAGE_HEIGHT).getValue()); + + assertNotNull(read.getEntryById(TIFF.TAG_ORIENTATION)); + assertEquals(1, read.getEntryById(TIFF.TAG_ORIENTATION).getValue()); + + assertNotNull(read.getEntryById(TIFF.TAG_ARTIST)); + assertEquals("Harald K.", read.getEntryById(TIFF.TAG_ARTIST).getValue()); + } + + @Test + public void testWriteMotorola() throws IOException { + ArrayList entries = new ArrayList<>(); + entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {}); + entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, TIFF.TYPE_LONG, Integer.MAX_VALUE)); + Directory directory = new AbstractDirectory(entries) {}; + + ByteArrayOutputStream output = new FastByteArrayOutputStream(1024); + ImageOutputStream imageStream = ImageIO.createImageOutputStream(output); + + imageStream.setByteOrder(ByteOrder.BIG_ENDIAN); // BE = Motorola + + new TIFFWriter().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 TIFFReader().read(new ByteArrayImageInputStream(data)); + + assertNotNull(read); + assertEquals(2, read.size()); + assertNotNull(read.getEntryById(TIFF.TAG_SOFTWARE)); + assertEquals("TwelveMonkeys ImageIO", read.getEntryById(TIFF.TAG_SOFTWARE).getValue()); + assertNotNull(read.getEntryById(TIFF.TAG_IMAGE_WIDTH)); + assertEquals((long) Integer.MAX_VALUE, read.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue()); + } + + @Test + public void testWriteIntel() throws IOException { + ArrayList entries = new ArrayList<>(); + entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {}); + entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, TIFF.TYPE_LONG, Integer.MAX_VALUE)); + Directory directory = new AbstractDirectory(entries) {}; + + ByteArrayOutputStream output = new FastByteArrayOutputStream(1024); + ImageOutputStream imageStream = ImageIO.createImageOutputStream(output); + + imageStream.setByteOrder(ByteOrder.LITTLE_ENDIAN); // LE = Intel + + new TIFFWriter().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 TIFFReader().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 { + TIFFEntry artist = new TIFFEntry(TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, "TwelveMonkeys ImageIO"); + + TIFFEntry subSubSubSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_LONG, new IFD(Collections.singletonList(artist))); + TIFFEntry subSubSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_LONG, new IFD(Collections.singletonList(subSubSubSubIFD))); + TIFFEntry subSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_LONG, new IFD(Collections.singletonList(subSubSubIFD))); + TIFFEntry subIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_LONG, new IFD(Collections.singletonList(subSubIFD))); + + Directory directory = new IFD(Collections.singletonList(subIFD)); + + ByteArrayOutputStream output = new FastByteArrayOutputStream(1024); + ImageOutputStream imageStream = ImageIO.createImageOutputStream(output); + + new TIFFWriter().write(directory, imageStream); + imageStream.flush(); + + assertEquals(output.size(), imageStream.getStreamPosition()); + + Directory read = new TIFFReader().read(new ByteArrayImageInputStream(output.toByteArray())); + + assertNotNull(read); + assertEquals(1, read.size()); + assertEquals(subIFD, read.getEntryById(TIFF.TAG_SUB_IFD)); // Recursively tests content! + } + + @Test + public void testReadWriteRead() throws IOException { + Directory original = createReader().read(getDataAsIIS()); + + ByteArrayOutputStream output = new FastByteArrayOutputStream(256); + ImageOutputStream imageOutput = ImageIO.createImageOutputStream(output); + + try { + createWriter().write(original, imageOutput); + } + finally { + imageOutput.close(); + } + + Directory read = createReader().read(new ByteArrayImageInputStream(output.toByteArray())); + + assertEquals(original, read); + } + + @Test + public void testComputeIFDSize() throws IOException { + ArrayList entries = new ArrayList<>(); + entries.add(new TIFFEntry(TIFF.TAG_ORIENTATION, TIFF.TYPE_SHORT, 1)); + entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, TIFF.TYPE_SHORT, 1600)); + entries.add(new AbstractEntry(TIFF.TAG_IMAGE_HEIGHT, 900) {}); + entries.add(new TIFFEntry(TIFF.TAG_ARTIST, TIFF.TYPE_ASCII, "Harald K.")); + entries.add(new AbstractEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO") {}); + + TIFFWriter 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 { + TIFFEntry artist = new TIFFEntry(TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, "TwelveMonkeys ImageIO"); + + TIFFEntry subSubSubSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_LONG, new IFD(Collections.singletonList(artist))); + TIFFEntry subSubSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_LONG, new IFD(Collections.singletonList(subSubSubSubIFD))); + TIFFEntry subSubIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_LONG, new IFD(Collections.singletonList(subSubSubIFD))); + TIFFEntry subIFD = new TIFFEntry(TIFF.TAG_SUB_IFD, TIFF.TYPE_LONG, new IFD(Collections.singletonList(subSubIFD))); + + List entries = Collections.singletonList(subIFD); + + TIFFWriter writer = createWriter(); + + ImageOutputStream stream = new NullImageOutputStream(); + writer.write(new IFD(entries), stream); + + assertEquals(stream.getStreamPosition(), writer.computeIFDSize(entries) + 12); + } + + private static class NullImageOutputStream extends ImageOutputStreamImpl { + @Override + public void write(int b) throws IOException { + streamPos++; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + streamPos += len; + } + + @Override + public int read() throws IOException { + throw new UnsupportedOperationException("Method read not implemented"); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + throw new UnsupportedOperationException("Method read not implemented"); + } + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/UnknownTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/UnknownTest.java similarity index 97% rename from imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/UnknownTest.java rename to imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/UnknownTest.java index 06239ba9..a700cc60 100644 --- a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/exif/UnknownTest.java +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/UnknownTest.java @@ -26,7 +26,7 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.twelvemonkeys.imageio.metadata.exif; +package com.twelvemonkeys.imageio.metadata.tiff; import com.twelvemonkeys.lang.ObjectAbstractTestCase; diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java index 8f314a04..8fe3c3c8 100644 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java @@ -29,7 +29,7 @@ package com.twelvemonkeys.imageio.plugins.psd; import com.twelvemonkeys.imageio.metadata.Directory; -import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; import javax.imageio.stream.ImageInputStream; import java.io.IOException; @@ -56,7 +56,7 @@ final class PSDEXIF1Data extends PSDImageResource { protected void readData(final ImageInputStream pInput) throws IOException { // This is in essence an embedded TIFF file. // TODO: Instead, read the byte data, store for later parsing (or better yet, store offset, and read on request) - directory = new EXIFReader().read(pInput); + directory = new TIFFReader().read(pInput); } @Override diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java index f59cb0b3..2e2e4901 100755 --- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java +++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java @@ -31,8 +31,8 @@ package com.twelvemonkeys.imageio.plugins.psd; import com.twelvemonkeys.imageio.AbstractMetadata; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; -import com.twelvemonkeys.imageio.metadata.exif.TIFF; import com.twelvemonkeys.imageio.metadata.iptc.IPTC; +import com.twelvemonkeys.imageio.metadata.tiff.TIFF; import com.twelvemonkeys.lang.StringUtil; import com.twelvemonkeys.util.FilterIterator; import org.w3c.dom.Node; @@ -369,12 +369,10 @@ public final class PSDMetadata extends AbstractMetadata { private Node createLayerInfoNode() { IIOMetadataNode layers = new IIOMetadataNode("Layers"); - IIOMetadataNode node; - for (PSDLayerInfo psdLayerInfo : layerInfo) { // TODO: Group in layer and use sub node for blend mode? - node = new IIOMetadataNode("LayerInfo"); + IIOMetadataNode node = new IIOMetadataNode("LayerInfo"); node.setAttribute("name", psdLayerInfo.getLayerName()); node.setAttribute("top", String.valueOf(psdLayerInfo.top)); node.setAttribute("left", String.valueOf(psdLayerInfo.left)); diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java index f6444464..3bff65f4 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFExtension.java @@ -28,6 +28,8 @@ package com.twelvemonkeys.imageio.plugins.tiff; +import com.twelvemonkeys.imageio.metadata.tiff.TIFF; + /** * TIFFExtension * @@ -81,7 +83,7 @@ interface TIFFExtension { /** * For use with Photometric: 5 (Separated), when image data is in a color space other than CMYK. - * See {@link com.twelvemonkeys.imageio.metadata.exif.TIFF#TAG_INK_NAMES InkNames} field for a + * See {@link TIFF#TAG_INK_NAMES InkNames} field for a * description of the inks to be used. */ int INKSET_NOT_CMYK = 2; diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadata.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadata.java index d652d81e..6de78164 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadata.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadata.java @@ -29,11 +29,12 @@ package com.twelvemonkeys.imageio.plugins.tiff; import com.twelvemonkeys.imageio.AbstractMetadata; -import com.twelvemonkeys.imageio.metadata.AbstractDirectory; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; -import com.twelvemonkeys.imageio.metadata.exif.Rational; -import com.twelvemonkeys.imageio.metadata.exif.TIFF; +import com.twelvemonkeys.imageio.metadata.tiff.IFD; +import com.twelvemonkeys.imageio.metadata.tiff.Rational; +import com.twelvemonkeys.imageio.metadata.tiff.TIFF; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry; import com.twelvemonkeys.lang.Validate; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -70,7 +71,7 @@ public final class TIFFImageMetadata extends AbstractMetadata { * or {@link #mergeTree(String, Node)} methods. */ public TIFFImageMetadata() { - this(new TIFFIFD(Collections.emptyList())); + this(new IFD(Collections.emptyList())); } /** @@ -94,7 +95,7 @@ public final class TIFFImageMetadata extends AbstractMetadata { * or {@link #mergeTree(String, Node)} methods. */ public TIFFImageMetadata(final Collection entries) { - this(new TIFFIFD(entries)); + this(new IFD(entries)); } protected IIOMetadataNode getNativeTree() { @@ -921,7 +922,7 @@ public final class TIFFImageMetadata extends AbstractMetadata { // TODO: Consistency validation? // Finally create a new IFD from merged values - ifd = new TIFFIFD(entries.values()); + ifd = new IFD(entries.values()); } @Override @@ -941,7 +942,7 @@ public final class TIFFImageMetadata extends AbstractMetadata { // TODO: Consistency validation? // Finally create a new IFD from merged values - ifd = new TIFFIFD(entries.values()); + ifd = new IFD(entries.values()); } private void mergeEntries(final String formatName, final Node root, final Map entries) throws IIOInvalidTreeException { @@ -1027,25 +1028,25 @@ public final class TIFFImageMetadata extends AbstractMetadata { int x = Math.round(xRes * scale * RATIONAL_SCALE_FACTOR); int y = Math.round(yRes * scale * RATIONAL_SCALE_FACTOR); - entries.put(TIFF.TAG_X_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(x, RATIONAL_SCALE_FACTOR))); - entries.put(TIFF.TAG_Y_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(y, RATIONAL_SCALE_FACTOR))); + entries.put(TIFF.TAG_X_RESOLUTION, new TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(x, RATIONAL_SCALE_FACTOR))); + entries.put(TIFF.TAG_Y_RESOLUTION, new TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(y, RATIONAL_SCALE_FACTOR))); entries.put(TIFF.TAG_RESOLUTION_UNIT, - new TIFFImageWriter.TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, resUnitValue)); + new TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, resUnitValue)); } else if (aspect != null) { if (aspect >= 1) { int v = Math.round(aspect * RATIONAL_SCALE_FACTOR); - entries.put(TIFF.TAG_X_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(v, RATIONAL_SCALE_FACTOR))); - entries.put(TIFF.TAG_Y_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(1))); + entries.put(TIFF.TAG_X_RESOLUTION, new TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(v, RATIONAL_SCALE_FACTOR))); + entries.put(TIFF.TAG_Y_RESOLUTION, new TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(1))); } else { int v = Math.round(RATIONAL_SCALE_FACTOR / aspect); - entries.put(TIFF.TAG_X_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(1))); - entries.put(TIFF.TAG_Y_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(v, RATIONAL_SCALE_FACTOR))); + entries.put(TIFF.TAG_X_RESOLUTION, new TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(1))); + entries.put(TIFF.TAG_Y_RESOLUTION, new TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(v, RATIONAL_SCALE_FACTOR))); } entries.put(TIFF.TAG_RESOLUTION_UNIT, - new TIFFImageWriter.TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, TIFFBaseline.RESOLUTION_UNIT_NONE)); + new TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, TIFFBaseline.RESOLUTION_UNIT_NONE)); } // Else give up... } @@ -1086,37 +1087,37 @@ public final class TIFFImageMetadata extends AbstractMetadata { // We do all comparisons in lower case, for compatibility keyword = keyword.toLowerCase(); - TIFFImageWriter.TIFFEntry entry; + TIFFEntry entry; if ("documentname".equals(keyword)) { - entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_DOCUMENT_NAME, TIFF.TYPE_ASCII, value); + entry = new TIFFEntry(TIFF.TAG_DOCUMENT_NAME, TIFF.TYPE_ASCII, value); } else if ("imagedescription".equals(keyword)) { - entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_IMAGE_DESCRIPTION, TIFF.TYPE_ASCII, value); + entry = new TIFFEntry(TIFF.TAG_IMAGE_DESCRIPTION, TIFF.TYPE_ASCII, value); } else if ("make".equals(keyword)) { - entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_MAKE, TIFF.TYPE_ASCII, value); + entry = new TIFFEntry(TIFF.TAG_MAKE, TIFF.TYPE_ASCII, value); } else if ("model".equals(keyword)) { - entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_MODEL, TIFF.TYPE_ASCII, value); + entry = new TIFFEntry(TIFF.TAG_MODEL, TIFF.TYPE_ASCII, value); } else if ("pagename".equals(keyword)) { - entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_PAGE_NAME, TIFF.TYPE_ASCII, value); + entry = new TIFFEntry(TIFF.TAG_PAGE_NAME, TIFF.TYPE_ASCII, value); } else if ("software".equals(keyword)) { - entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, value); + entry = new TIFFEntry(TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, value); } else if ("artist".equals(keyword)) { - entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_ARTIST, TIFF.TYPE_ASCII, value); + entry = new TIFFEntry(TIFF.TAG_ARTIST, TIFF.TYPE_ASCII, value); } else if ("hostcomputer".equals(keyword)) { - entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_HOST_COMPUTER, TIFF.TYPE_ASCII, value); + entry = new TIFFEntry(TIFF.TAG_HOST_COMPUTER, TIFF.TYPE_ASCII, value); } else if ("inknames".equals(keyword)) { - entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_INK_NAMES, TIFF.TYPE_ASCII, value); + entry = new TIFFEntry(TIFF.TAG_INK_NAMES, TIFF.TYPE_ASCII, value); } else if ("copyright".equals(keyword)) { - entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_COPYRIGHT, TIFF.TYPE_ASCII, value); + entry = new TIFFEntry(TIFF.TAG_COPYRIGHT, TIFF.TYPE_ASCII, value); } else { continue; @@ -1148,7 +1149,7 @@ public final class TIFFImageMetadata extends AbstractMetadata { entries.add(toEntry(nodes.item(i))); } - return new TIFFIFD(entries); + return new IFD(entries); } private Entry toEntry(final Node node) throws IIOInvalidTreeException { @@ -1158,14 +1159,14 @@ public final class TIFFImageMetadata extends AbstractMetadata { int tag = Integer.parseInt(getAttribute(node, "parentTagNumber")); Directory subIFD = toIFD(node); - return new TIFFImageWriter.TIFFEntry(tag, TIFF.TYPE_IFD, subIFD); + return new TIFFEntry(tag, TIFF.TYPE_IFD, subIFD); } else if (name.equals("TIFFField")) { int tag = Integer.parseInt(getAttribute(node, "number")); short type = getTIFFType(node); Object value = getValue(node, type); - return value != null ? new TIFFImageWriter.TIFFEntry(tag, type, value) : null; + return value != null ? new TIFFEntry(tag, type, value) : null; } else { throw new IIOInvalidTreeException("Expected \"TIFFIFD\" or \"TIFFField\" node: " + name, node); @@ -1337,11 +1338,4 @@ public final class TIFFImageMetadata extends AbstractMetadata { public Entry getTIFFField(final int tagNumber) { return ifd.getEntryById(tagNumber); } - - // TODO: Replace with IFD class when moved to new package and made public! - private final static class TIFFIFD extends AbstractDirectory { - public TIFFIFD(final Collection entries) { - super(entries); - } - } } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java index 9f8c341d..2276b5ea 100755 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java @@ -36,12 +36,12 @@ import com.twelvemonkeys.imageio.color.YCbCrConverter; import com.twelvemonkeys.imageio.metadata.CompoundDirectory; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; -import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; -import com.twelvemonkeys.imageio.metadata.exif.Rational; -import com.twelvemonkeys.imageio.metadata.exif.TIFF; import com.twelvemonkeys.imageio.metadata.iptc.IPTCReader; import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; import com.twelvemonkeys.imageio.metadata.psd.PSDReader; +import com.twelvemonkeys.imageio.metadata.tiff.Rational; +import com.twelvemonkeys.imageio.metadata.tiff.TIFF; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; import com.twelvemonkeys.imageio.metadata.xmp.XMPReader; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.stream.SubImageInputStream; @@ -171,7 +171,7 @@ public final class TIFFImageReader extends ImageReaderBase { } if (IFDs == null) { - IFDs = (CompoundDirectory) new EXIFReader().read(imageInput); // NOTE: Sets byte order as a side effect + IFDs = (CompoundDirectory) new TIFFReader().read(imageInput); // NOTE: Sets byte order as a side effect if (DEBUG) { System.err.println("Byte order: " + imageInput.getByteOrder()); diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java index 9e96e70f..d763d2cf 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderSpi.java @@ -28,7 +28,7 @@ package com.twelvemonkeys.imageio.plugins.tiff; -import com.twelvemonkeys.imageio.metadata.exif.TIFF; +import com.twelvemonkeys.imageio.metadata.tiff.TIFF; import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; import javax.imageio.spi.ImageReaderSpi; diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java index b82dae63..847f57e0 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java @@ -30,12 +30,12 @@ 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.Directory; 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.metadata.tiff.Rational; +import com.twelvemonkeys.imageio.metadata.tiff.TIFF; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFWriter; import com.twelvemonkeys.imageio.stream.SubImageOutputStream; import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.io.enc.EncoderStream; @@ -54,7 +54,6 @@ import java.awt.color.ColorSpace; import java.awt.color.ICC_ColorSpace; import java.awt.image.*; import java.io.*; -import java.lang.reflect.Array; import java.nio.ByteBuffer; import java.util.*; import java.util.zip.Deflater; @@ -111,7 +110,7 @@ public final class TIFFImageWriter extends ImageWriterBase { /** * Metadata writer for sequence writing */ - private EXIFWriter sequenceExifWriter = null; + private TIFFWriter sequenceTiffWriter = null; /** * Position of last IFD Pointer on active sequence writing @@ -129,87 +128,21 @@ public final class TIFFImageWriter extends ImageWriterBase { // TODO: Allow appending/partly overwrite of existing file... } - static final class TIFFEntry extends AbstractEntry { - // TODO: Expose a merge of this and the EXIFEntry class... - private final short type; - - private static short guessType(final Object val) { - // TODO: This code is duplicated in EXIFWriter.getType, needs refactor! - Object value = Validate.notNull(val); - - 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 guessType not implemented for value of type %s", value.getClass())); - } - - TIFFEntry(final int identifier, final Object value) { - this(identifier, guessType(value), value); - } - - TIFFEntry(int identifier, short type, Object value) { - super(identifier, value); - this.type = type; - } - - @Override - public String getTypeName() { - return TIFF.TYPE_NAMES[type]; - } - } - @Override public void write(final IIOMetadata streamMetadata, final IIOImage image, final ImageWriteParam param) throws IOException { assertOutput(); configureStreamByteOrder(streamMetadata, imageOutput); // TODO: Make TIFFEntry and possibly TIFFDirectory? public - EXIFWriter exifWriter = new EXIFWriter(); - exifWriter.writeTIFFHeader(imageOutput); + TIFFWriter tiffWriter = new TIFFWriter(); + tiffWriter.writeTIFFHeader(imageOutput); - writePage(image, param, exifWriter, imageOutput.getStreamPosition()); + writePage(image, param, tiffWriter, imageOutput.getStreamPosition()); imageOutput.flush(); } - private long writePage(IIOImage image, ImageWriteParam param, EXIFWriter exifWriter, long lastIFDPointerOffset) + private long writePage(IIOImage image, ImageWriteParam param, TIFFWriter tiffWriter, long lastIFDPointerOffset) throws IOException { RenderedImage renderedImage = image.getRenderedImage(); @@ -382,14 +315,14 @@ public final class TIFFImageWriter extends ImageWriterBase { // This implementation, allows semi-streaming-compatible uncompressed TIFFs long streamPosition = imageOutput.getStreamPosition(); - long ifdSize = exifWriter.computeIFDSize(entries.values()); + long ifdSize = tiffWriter.computeIFDSize(entries.values()); long stripOffset = streamPosition + 4 + ifdSize + 4; long stripByteCount = (renderedImage.getWidth() * renderedImage.getHeight() * pixelSize + 7) / 8; entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, stripOffset)); entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, stripByteCount)); - long ifdPointer = exifWriter.writeIFD(entries.values(), imageOutput); // NOTE: Writer takes case of ordering tags + long ifdPointer = tiffWriter.writeIFD(entries.values(), imageOutput); // NOTE: Writer takes case of ordering tags nextIFDPointerOffset = imageOutput.getStreamPosition(); // If we have a previous IFD, update pointer @@ -438,7 +371,7 @@ public final class TIFFImageWriter extends ImageWriterBase { entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, stripOffset)); entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, stripByteCount)); - long ifdPointer = exifWriter.writeIFD(entries.values(), imageOutput); // NOTE: Writer takes case of ordering tags + long ifdPointer = tiffWriter.writeIFD(entries.values(), imageOutput); // NOTE: Writer takes case of ordering tags nextIFDPointerOffset = imageOutput.getStreamPosition(); @@ -958,8 +891,8 @@ public final class TIFFImageWriter extends ImageWriterBase { // Ignore streamMetadata. ByteOrder is determined from OutputStream assertOutput(); isWritingSequence = true; - sequenceExifWriter = new EXIFWriter(); - sequenceExifWriter.writeTIFFHeader(imageOutput); + sequenceTiffWriter = new TIFFWriter(); + sequenceTiffWriter.writeTIFFHeader(imageOutput); sequenceLastIFDPos = imageOutput.getStreamPosition(); } @@ -973,7 +906,7 @@ public final class TIFFImageWriter extends ImageWriterBase { imageOutput.flushBefore(sequenceLastIFDPos); } - sequenceLastIFDPos = writePage(image, param, sequenceExifWriter, sequenceLastIFDPos); + sequenceLastIFDPos = writePage(image, param, sequenceTiffWriter, sequenceLastIFDPos); } @Override @@ -983,7 +916,7 @@ public final class TIFFImageWriter extends ImageWriterBase { } isWritingSequence = false; - sequenceExifWriter = null; + sequenceTiffWriter = null; sequenceLastIFDPos = -1; imageOutput.flush(); } @@ -993,7 +926,7 @@ public final class TIFFImageWriter extends ImageWriterBase { super.resetMembers(); isWritingSequence = false; - sequenceExifWriter = null; + sequenceTiffWriter = null; sequenceLastIFDPos = -1; } diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadataTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadataTest.java index 242044d0..4256230c 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadataTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadataTest.java @@ -2,9 +2,10 @@ package com.twelvemonkeys.imageio.plugins.tiff; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; -import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; -import com.twelvemonkeys.imageio.metadata.exif.Rational; -import com.twelvemonkeys.imageio.metadata.exif.TIFF; +import com.twelvemonkeys.imageio.metadata.tiff.Rational; +import com.twelvemonkeys.imageio.metadata.tiff.TIFF; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi; import com.twelvemonkeys.lang.StringUtil; import org.junit.Test; @@ -49,7 +50,7 @@ public class TIFFImageMetadataTest { // TODO: Candidate abstract super method private IIOMetadata createMetadata(final String resource) throws IOException { try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource(resource))) { - Directory ifd = new EXIFReader().read(input); + Directory ifd = new TIFFReader().read(input); // System.err.println("ifd: " + ifd); return new TIFFImageMetadata(ifd); } @@ -477,9 +478,9 @@ public class TIFFImageMetadataTest { @Test public void testStandardChromaSamplesPerPixel() { Set entries = new HashSet<>(); - entries.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFFBaseline.PHOTOMETRIC_RGB)); - entries.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 4)); - entries.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, new int[] {8, 8, 8})); // This is incorrect, just making sure the correct value is selected + entries.add(new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFFBaseline.PHOTOMETRIC_RGB)); + entries.add(new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 4)); + entries.add(new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, new int[] {8, 8, 8})); // This is incorrect, just making sure the correct value is selected IIOMetadataNode chromaNode = new TIFFImageMetadata(entries).getStandardChromaNode(); assertNotNull(chromaNode); @@ -491,8 +492,8 @@ public class TIFFImageMetadataTest { @Test public void testStandardChromaSamplesPerPixelFallbackBitsPerSample() { Set entries = new HashSet<>(); - entries.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFFBaseline.PHOTOMETRIC_RGB)); - entries.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, new int[] {8, 8, 8})); + entries.add(new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFFBaseline.PHOTOMETRIC_RGB)); + entries.add(new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, new int[] {8, 8, 8})); IIOMetadataNode chromaNode = new TIFFImageMetadata(entries).getStandardChromaNode(); assertNotNull(chromaNode); @@ -504,7 +505,7 @@ public class TIFFImageMetadataTest { @Test public void testStandardChromaSamplesPerPixelFallbackDefault() { Set entries = new HashSet<>(); - entries.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO)); + entries.add(new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO)); IIOMetadataNode chromaNode = new TIFFImageMetadata(entries).getStandardChromaNode(); assertNotNull(chromaNode); @@ -515,7 +516,7 @@ public class TIFFImageMetadataTest { @Test public void testStandardDataBitsPerSampleFallbackDefault() { Set entries = new HashSet<>(); - entries.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO)); + entries.add(new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO)); IIOMetadataNode dataNode = new TIFFImageMetadata(entries).getStandardDataNode(); assertNotNull(dataNode); @@ -526,7 +527,7 @@ public class TIFFImageMetadataTest { @Test public void testStandardNodeSamplesPerPixelFallbackDefault() { Set entries = new HashSet<>(); - entries.add(new TIFFImageWriter.TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFFBaseline.PHOTOMETRIC_RGB)); + entries.add(new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFFBaseline.PHOTOMETRIC_RGB)); // Just to make sure we haven't accidentally missed something IIOMetadataNode standardTree = (IIOMetadataNode) new TIFFImageMetadata(entries).getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); @@ -656,4 +657,4 @@ public class TIFFImageMetadataTest { return builder.toString(); } -} \ No newline at end of file +} diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java index 59510ac3..52a9d83a 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java @@ -30,9 +30,9 @@ package com.twelvemonkeys.imageio.plugins.tiff; import com.twelvemonkeys.imageio.metadata.Directory; import com.twelvemonkeys.imageio.metadata.Entry; -import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; -import com.twelvemonkeys.imageio.metadata.exif.Rational; -import com.twelvemonkeys.imageio.metadata.exif.TIFF; +import com.twelvemonkeys.imageio.metadata.tiff.Rational; +import com.twelvemonkeys.imageio.metadata.tiff.TIFF; +import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.util.ImageWriterAbstractTestCase; import com.twelvemonkeys.io.FastByteArrayOutputStream; @@ -131,7 +131,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase { assertTrue("No image data written", buffer.size() > 0); - Directory ifds = new EXIFReader().read(new ByteArrayImageInputStream(buffer.toByteArray())); + Directory ifds = new TIFFReader().read(new ByteArrayImageInputStream(buffer.toByteArray())); Entry resolutionUnit = ifds.getEntryById(TIFF.TAG_RESOLUTION_UNIT); assertNotNull(resolutionUnit); @@ -179,7 +179,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase { assertTrue("No image data written", buffer.size() > 0); - Directory ifds = new EXIFReader().read(new ByteArrayImageInputStream(buffer.toByteArray())); + Directory ifds = new TIFFReader().read(new ByteArrayImageInputStream(buffer.toByteArray())); Entry software = ifds.getEntryById(TIFF.TAG_SOFTWARE); assertNotNull(software); assertEquals(softwareString, software.getValueAsString()); @@ -227,7 +227,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase { assertTrue("No image data written", buffer.size() > 0); - Directory ifds = new EXIFReader().read(new ByteArrayImageInputStream(buffer.toByteArray())); + Directory ifds = new TIFFReader().read(new ByteArrayImageInputStream(buffer.toByteArray())); Entry resolutionUnit = ifds.getEntryById(TIFF.TAG_RESOLUTION_UNIT); assertNotNull(resolutionUnit); @@ -278,7 +278,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase { assertTrue("No image data written", buffer.size() > 0); - Directory ifds = new EXIFReader().read(new ByteArrayImageInputStream(buffer.toByteArray())); + Directory ifds = new TIFFReader().read(new ByteArrayImageInputStream(buffer.toByteArray())); Entry software = ifds.getEntryById(TIFF.TAG_SOFTWARE); assertNotNull(software); assertEquals(softwareString, software.getValueAsString());