mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-05 04:25:29 -04:00
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:
parent
2a282cf8e4
commit
5d3fb34e49
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
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<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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user