From 5d3fb34e49928f43bdf3cb3df38fbd8b9707977b Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Mon, 12 Dec 2011 10:42:40 +0100 Subject: [PATCH] Various fixes for metadata parsing. - Added more TIFF/EXIF tags - Clean-up of JPEG segment reading - Better toString in general and XMP specific --- .../imageio/metadata/AbstractEntry.java | 6 +- .../imageio/metadata/exif/EXIF.java | 55 +++++++ .../imageio/metadata/exif/EXIFEntry.java | 28 +++- .../imageio/metadata/exif/EXIFReader.java | 2 + .../imageio/metadata/exif/TIFF.java | 1 + .../imageio/metadata/jpeg/JPEGSegment.java | 22 ++- .../metadata/jpeg/JPEGSegmentUtil.java | 139 +++++++++++++++--- .../imageio/metadata/xmp/XMPEntry.java | 8 + .../metadata/jpeg/JPEGSegmentTest.java | 8 +- 9 files changed, 238 insertions(+), 31 deletions(-) diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java index f10783a3..f15bf729 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java @@ -109,6 +109,10 @@ public abstract class AbstractEntry implements Entry { return String.valueOf(value) + " (" + valueCount() + ")"; } + if (value.getClass().isArray() && Array.getLength(value) == 1) { + return String.valueOf(Array.get(value, 0)); + } + return String.valueOf(value); } @@ -129,10 +133,8 @@ public abstract class AbstractEntry implements Entry { return 1; } - /// Object - @Override public int hashCode() { return identifier.hashCode() + 31 * value.hashCode(); diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIF.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIF.java index 46e22826..9ebda2ab 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIF.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIF.java @@ -36,7 +36,62 @@ package com.twelvemonkeys.imageio.metadata.exif; * @version $Id: EXIF.java,v 1.0 Nov 11, 2009 5:36:04 PM haraldk Exp$ */ public interface EXIF { + // See http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html + int TAG_EXPOSURE_TIME = 33434; + int TAG_F_NUMBER = 33437; + int TAG_EXPOSURE_PROGRAM = 34850; + int TAG_SPECTRAL_SENSITIVITY = 34852; + int TAG_ISO_SPEED_RATINGS = 34855; + int TAG_OECF = 34856; + int TAG_EXIF_VERSION = 36864; + int TAG_DATE_TIME_ORIGINAL = 36867; + int TAG_DATE_TIME_DIGITIZED = 36868; + int TAG_COMPONENTS_CONFIGURATION = 37121; + int TAG_COMPRESSED_BITS_PER_PIXEL = 37122; + int TAG_SHUTTER_SPEED_VALUE = 37377; + int TAG_APERTURE_VALUE = 37378; + int TAG_BRIGHTNESS_VALUE = 37379; + int TAG_EXPOSURE_BIAS_VALUE = 37380; + int TAG_MAX_APERTURE_VALUE = 37381; + int TAG_SUBJECT_DISTANCE = 37382; + int TAG_METERING_MODE = 37383; + int TAG_LIGHT_SOURCE = 37384; + int TAG_FLASH = 37385; + int TAG_FOCAL_LENGTH = 37386; + int TAG_IMAGE_NUMBER = 37393; + int TAG_SUBJECT_AREA = 37396; + int TAG_MAKER_NOTE = 37500; + int TAG_USER_COMMENT = 37510; + int TAG_SUBSEC_TIME = 37520; + int TAG_SUBSEC_TIME_ORIGINAL = 37521; + int TAG_SUBSEC_TIME_DIGITIZED = 37522; + int TAG_FLASHPIX_VERSION = 40960; int TAG_COLOR_SPACE = 40961; int TAG_PIXEL_X_DIMENSION = 40962; int TAG_PIXEL_Y_DIMENSION = 40963; + int TAG_RELATED_SOUND_FILE = 40964; + int TAG_FLASH_ENERGY = 41483; + int TAG_SPATIAL_FREQUENCY_RESPONSE = 41484; + int TAG_FOCAL_PLANE_X_RESOLUTION = 41486; + int TAG_FOCAL_PLANE_Y_RESOLUTION = 41487; + int TAG_FOCAL_PLANE_RESOLUTION_UNIT = 41488; + int TAG_SUBJECT_LOCATION = 41492; + int TAG_EXPOSURE_INDEX = 41493; + int TAG_SENSING_METHOD = 41495; + int TAG_FILE_SOURCE = 41728; + int TAG_SCENE_TYPE = 41729; + int TAG_CFA_PATTERN = 41730; + int TAG_CUSTOM_RENDERED = 41985; + int TAG_EXPOSURE_MODE = 41986; + int TAG_WHITE_BALANCE = 41987; + int TAG_DIGITAL_ZOOM_RATIO = 41988; + int TAG_FOCAL_LENGTH_IN_35_MM_FILM = 41989; + int TAG_SCENE_CAPTURE_TYPE = 41990; + int TAG_GAIN_CONTROL = 41991; + int TAG_CONTRAST = 41992; + int TAG_SATURATION = 41993; + int TAG_SHARPNESS = 41994; + int TAG_DEVICE_SETTING_DESCRIPTION = 41995; + int TAG_SUBJECT_DISTANCE_RANGE = 41996; + int TAG_IMAGE_UNIQUE_ID = 42016; } 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/exif/EXIFEntry.java index 2c401419..d40d29c8 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/exif/EXIFEntry.java @@ -66,7 +66,7 @@ final class EXIFEntry extends AbstractEntry { case TIFF.TAG_PHOTOSHOP: return "Adobe"; case TIFF.TAG_ICC_PROFILE: - return "ICC Profile"; + return "ICCProfile"; case TIFF.TAG_IMAGE_WIDTH: return "ImageWidth"; @@ -86,15 +86,41 @@ final class EXIFEntry extends AbstractEntry { return "JPEGInterchangeFormat"; case TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH: return "JPEGInterchangeFormatLength"; + case TIFF.TAG_MAKE: + return "Make"; + case TIFF.TAG_MODEL: + return "Model"; case TIFF.TAG_SOFTWARE: return "Software"; case TIFF.TAG_DATE_TIME: return "DateTime"; case TIFF.TAG_ARTIST: return "Artist"; + case TIFF.TAG_HOST_COMPUTER: + return "HostComputer"; case TIFF.TAG_COPYRIGHT: return "Copyright"; + case EXIF.TAG_EXPOSURE_TIME: + return "ExposureTime"; + case EXIF.TAG_F_NUMBER: + return "FNUmber"; + case EXIF.TAG_EXPOSURE_PROGRAM: + return "ExposureProgram"; + case EXIF.TAG_ISO_SPEED_RATINGS: + return "ISOSpeedRatings"; + + case EXIF.TAG_EXIF_VERSION: + return "ExifVersion"; + case EXIF.TAG_DATE_TIME_ORIGINAL: + return "DateTimeOriginal"; + case EXIF.TAG_DATE_TIME_DIGITIZED: + return "DateTimeDigitized"; + case EXIF.TAG_IMAGE_NUMBER: + return "ImageNumber"; + case EXIF.TAG_USER_COMMENT: + return "UserComment"; + case EXIF.TAG_COLOR_SPACE: return "ColorSpace"; case EXIF.TAG_PIXEL_X_DIMENSION: 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 17d5badf..e5ef402e 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 @@ -59,6 +59,7 @@ public final class EXIFReader extends MetadataReader { public Directory read(final ImageInputStream input) throws IOException { byte[] bom = new byte[2]; input.readFully(bom); + if (bom[0] == 'I' && bom[1] == 'I') { input.setByteOrder(ByteOrder.LITTLE_ENDIAN); } @@ -102,6 +103,7 @@ public final class EXIFReader extends MetadataReader { } // TODO: Make what sub-IFDs to parse optional? Or leave this to client code? At least skip the non-TIFF data? + // TODO: Put it in the constructor? readSubdirectories(pInput, entries, Arrays.asList(TIFF.TAG_EXIF_IFD, TIFF.TAG_GPS_IFD, TIFF.TAG_INTEROP_IFD // , TIFF.TAG_IPTC, TIFF.TAG_XMP 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 00194d2e..00a954fd 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 @@ -131,6 +131,7 @@ public interface TIFF { int TAG_MODEL = 272; int TAG_SOFTWARE = 305; int TAG_ARTIST = 315; + int TAG_HOST_COMPUTER = 316; int TAG_COPYRIGHT = 33432; int TAG_SUB_IFD = 330; diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java index 57711e61..2d667b6c 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegment.java @@ -43,16 +43,19 @@ import java.util.Arrays; public final class JPEGSegment implements Serializable { final int marker; final byte[] data; + final int length; + private transient String id; - JPEGSegment(int marker, byte[] data) { + JPEGSegment(int marker, byte[] data, int length) { this.marker = marker; this.data = data; + this.length = length; } int segmentLength() { // This is the length field as read from the stream - return data != null ? data.length + 2 : 0; + return length; } public int marker() { @@ -61,7 +64,7 @@ public final class JPEGSegment implements Serializable { public String identifier() { if (id == null) { - if (marker >= 0xFFE0 && marker <= 0xFFEF) { + if (isAppSegmentMarker(marker)) { // Only for APPn markers id = JPEGSegmentUtil.asNullTerminatedAsciiString(data, 0); } @@ -70,6 +73,10 @@ public final class JPEGSegment implements Serializable { return id; } + static boolean isAppSegmentMarker(final int marker) { + return marker >= 0xFFE0 && marker <= 0xFFEF; + } + public InputStream data() { return data != null ? new ByteArrayInputStream(data, offset(), length()) : null; } @@ -80,26 +87,31 @@ public final class JPEGSegment implements Serializable { private int offset() { String identifier = identifier(); + return identifier == null ? 0 : identifier.length() + 1; } @Override public String toString() { String identifier = identifier(); + if (identifier != null) { return String.format("JPEGSegment[%04x/%s size: %d]", marker, identifier, segmentLength()); } + return String.format("JPEGSegment[%04x size: %d]", marker, segmentLength()); } @Override public int hashCode() { String identifier = identifier(); + return marker() << 16 | (identifier != null ? identifier.hashCode() : 0) & 0xFFFF; } @Override - public boolean equals(Object other) { - return other instanceof JPEGSegment && ((JPEGSegment) other).marker == marker && Arrays.equals(((JPEGSegment) other).data, data); + public boolean equals(final Object other) { + return other instanceof JPEGSegment && + ((JPEGSegment) other).marker == marker && Arrays.equals(((JPEGSegment) other).data, data); } } 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 d5e2d09b..e1b089a7 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 @@ -28,9 +28,17 @@ package com.twelvemonkeys.imageio.metadata.jpeg; +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; +import com.twelvemonkeys.imageio.metadata.xmp.XMP; +import com.twelvemonkeys.imageio.metadata.xmp.XMPReader; + import javax.imageio.IIOException; +import javax.imageio.ImageIO; import javax.imageio.stream.ImageInputStream; import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.nio.charset.Charset; import java.util.*; @@ -44,20 +52,10 @@ import java.util.*; public final class JPEGSegmentUtil { public static final List ALL_IDS = Collections.unmodifiableList(new AllIdsList()); public static final Map> ALL_SEGMENTS = Collections.unmodifiableMap(new AllSegmentsMap()); - public static final Map> APP_SEGMENTS = Collections.unmodifiableMap(createAppSegmentsMap()); + public static final Map> APP_SEGMENTS = Collections.unmodifiableMap(new AllAppSegmentsMap()); private JPEGSegmentUtil() {} - private static Map> createAppSegmentsMap() { - Map> identifiers = new HashMap>(); - - for (int i = 0xFFE0; i <= 0xFFEF; i++) { - identifiers.put(i, JPEGSegmentUtil.ALL_IDS); - } - - return identifiers; - } - /** * Reads the requested JPEG segments from the stream. * The stream position must be directly before the SOI marker, and only segments for the current image is read. @@ -97,7 +95,6 @@ public final class JPEGSegmentUtil { JPEGSegment segment; try { while (!isImageDone(segment = readSegment(stream, segmentIdentifiers))) { -// while (!isImageDone(segment = readSegment(stream, ALL_SEGMENTS))) { // System.err.println("segment: " + segment); if (isRequested(segment, segmentIdentifiers)) { @@ -119,9 +116,8 @@ public final class JPEGSegmentUtil { } private static boolean isRequested(JPEGSegment segment, Map> segmentIdentifiers) { - return segmentIdentifiers == ALL_SEGMENTS || - (segmentIdentifiers.containsKey(segment.marker) && (segmentIdentifiers.get(segment.marker) == ALL_IDS || - (segment.identifier() == null && segmentIdentifiers.get(segment.marker) == null || containsSafe(segment, segmentIdentifiers)))); + return (segmentIdentifiers.containsKey(segment.marker) && + (segment.identifier() == null && segmentIdentifiers.get(segment.marker) == null || containsSafe(segment, segmentIdentifiers))); } private static boolean containsSafe(JPEGSegment segment, Map> segmentIdentifiers) { @@ -160,16 +156,31 @@ public final class JPEGSegmentUtil { byte[] data; - if (segmentIdentifiers == ALL_SEGMENTS || segmentIdentifiers.containsKey(marker)) { + if (segmentIdentifiers.containsKey(marker)) { data = new byte[length - 2]; stream.readFully(data); } else { - data = null; - stream.skipBytes(length - 2); + if (JPEGSegment.isAppSegmentMarker(marker)) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(32); + int read; + + // NOTE: Read until null-termination (0) or EOF + while ((read = stream.read()) > 0) { + buffer.write(read); + } + + data = buffer.toByteArray(); + + stream.skipBytes(length - 3 - data.length); + } + else { + data = null; + stream.skipBytes(length - 2); + } } - return new JPEGSegment(marker, data); + return new JPEGSegment(marker, data, length); } private static class AllIdsList extends ArrayList { @@ -177,6 +188,11 @@ public final class JPEGSegmentUtil { public String toString() { return "[All ids]"; } + + @Override + public boolean contains(Object o) { + return true; + } } private static class AllSegmentsMap extends HashMap> { @@ -184,5 +200,90 @@ public final class JPEGSegmentUtil { public String toString() { return "{All segments}"; } + + @Override + public List get(Object key) { + return key instanceof Integer && JPEGSegment.isAppSegmentMarker((Integer) key) ? ALL_IDS : null; + + } + + @Override + public boolean containsKey(Object key) { + return true; + } + } + + private static class AllAppSegmentsMap extends HashMap> { + @Override + public String toString() { + return "{All APPn segments}"; + } + + @Override + public List get(Object key) { + return containsKey(key) ? ALL_IDS : null; + + } + + @Override + public boolean containsKey(Object key) { + return key instanceof Integer && JPEGSegment.isAppSegmentMarker((Integer) key); + } + } + + public static void main(String[] args) throws IOException { + List segments = readSegments(ImageIO.createImageInputStream(new File(args[0])), ALL_SEGMENTS); + + for (JPEGSegment segment : segments) { + System.err.println("segment: " + segment); + + if ("Exif".equals(segment.identifier())) { + InputStream data = segment.data(); + //noinspection ResultOfMethodCallIgnored + data.read(); // Pad + + ImageInputStream stream = ImageIO.createImageInputStream(data); + + // Root entry is TIFF, that contains the EXIF sub-IFD + Directory tiff = new EXIFReader().read(stream); + System.err.println("EXIF: " + tiff); + } + else if (XMP.NS_XAP.equals(segment.identifier())) { + Directory xmp = new XMPReader().read(ImageIO.createImageInputStream(segment.data())); + System.err.println("XMP: " + xmp); + } + else if ("Photoshop 3.0".equals(segment.identifier())) { + // TODO: It's probably a good idea to move some of the Photoshop ImageResource parsing code + // to the metadata sub project, as it may be contained in other formats (such as JFIF). + // TODO: The "Photoshop 3.0" segment contains several image resources, of which one might contain + // IPTC metadata. Probably duplicated in the XMP though... + try { + Class cl = Class.forName("com.twelvemonkeys.imageio.plugins.psd.PSDImageResource"); + Method method = cl.getMethod("read", ImageInputStream.class); + method.setAccessible(true); + ImageInputStream stream = ImageIO.createImageInputStream(segment.data()); + + while (true) { + try { + Object photoShop = method.invoke(null, stream); + System.err.println("PhotoShop: " + photoShop); + } + catch (InvocationTargetException e) { + if (e.getTargetException() instanceof EOFException) { + break; + } + } + } + } + catch (Exception ignore) { + } + } + else if ("ICC_PROFILE".equals(segment.identifier())) { + // Skip + } + else { + System.err.println(EXIFReader.HexDump.dump(segment.data)); + } + } } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPEntry.java index 48c760c9..98163b69 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/xmp/XMPEntry.java @@ -54,4 +54,12 @@ final class XMPEntry extends AbstractEntry { public String getFieldName() { return fieldName != null ? fieldName : XMP.DEFAULT_NS_MAPPING.get(getIdentifier()); } + + @Override + public String toString() { + String type = getTypeName(); + String typeStr = type != null ? " (" + type + ")" : ""; + + return String.format("%s: %s%s", getIdentifier(), getValueAsString(), typeStr); + } } diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentTest.java index 85be7581..42a9abe5 100644 --- a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentTest.java +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentTest.java @@ -48,7 +48,7 @@ public class JPEGSegmentTest extends ObjectAbstractTestCase { byte[] bytes = new byte[14]; System.arraycopy("JFIF".getBytes(Charset.forName("ascii")), 0, bytes, 0, 4); - JPEGSegment segment = new JPEGSegment(0xFFE0, bytes); + JPEGSegment segment = new JPEGSegment(0xFFE0, bytes, 16); assertEquals(0xFFE0, segment.marker()); assertEquals("JFIF", segment.identifier()); @@ -60,7 +60,7 @@ public class JPEGSegmentTest extends ObjectAbstractTestCase { public void testToStringAppSegment() { byte[] bytes = new byte[14]; System.arraycopy("JFIF".getBytes(Charset.forName("ascii")), 0, bytes, 0, 4); - JPEGSegment segment = new JPEGSegment(0xFFE0, bytes); + JPEGSegment segment = new JPEGSegment(0xFFE0, bytes, 16); assertEquals("JPEGSegment[ffe0/JFIF size: 16]", segment.toString()); } @@ -68,7 +68,7 @@ public class JPEGSegmentTest extends ObjectAbstractTestCase { @Test public void testToStringNonAppSegment() { byte[] bytes = new byte[40]; - JPEGSegment segment = new JPEGSegment(0xFFC4, bytes); + JPEGSegment segment = new JPEGSegment(0xFFC4, bytes, 42); assertEquals("JPEGSegment[ffc4 size: 42]", segment.toString()); } @@ -77,6 +77,6 @@ public class JPEGSegmentTest extends ObjectAbstractTestCase { protected Object makeObject() { byte[] bytes = new byte[11]; System.arraycopy("Exif".getBytes(Charset.forName("ascii")), 0, bytes, 0, 4); - return new JPEGSegment(0xFFE1, bytes); + return new JPEGSegment(0xFFE1, bytes, 16); } }