Various fixes for metadata parsing.

- Added more TIFF/EXIF tags
- Clean-up of JPEG segment reading
- Better toString in general and XMP specific
This commit is contained in:
Harald Kuhr 2011-12-12 10:42:40 +01:00
parent 2a282cf8e4
commit 5d3fb34e49
9 changed files with 238 additions and 31 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String> ALL_IDS = Collections.unmodifiableList(new AllIdsList());
public static final Map<Integer, List<String>> ALL_SEGMENTS = Collections.unmodifiableMap(new AllSegmentsMap());
public static final Map<Integer, List<String>> APP_SEGMENTS = Collections.unmodifiableMap(createAppSegmentsMap());
public static final Map<Integer, List<String>> APP_SEGMENTS = Collections.unmodifiableMap(new AllAppSegmentsMap());
private JPEGSegmentUtil() {}
private static Map<Integer, List<String>> createAppSegmentsMap() {
Map<Integer, List<String>> identifiers = new HashMap<Integer, List<String>>();
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<Integer, List<String>> 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<Integer, List<String>> 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 {
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<String> {
@ -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<Integer, List<String>> {
@ -184,5 +200,90 @@ public final class JPEGSegmentUtil {
public String toString() {
return "{All segments}";
}
@Override
public List<String> 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<Integer, List<String>> {
@Override
public String toString() {
return "{All APPn segments}";
}
@Override
public List<String> 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<JPEGSegment> 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));
}
}
}
}

View File

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

View File

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