mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2026-05-01 00:00:02 -04:00
Work in progress for PSD metadata support:
- Refactored metadata support - Moved standard metadata support (EXIF, IPTC & XMP) to separate module - Changes to PSD metadata implementation
This commit is contained in:
+116
@@ -0,0 +1,116 @@
|
||||
package com.twelvemonkeys.imageio.metadata;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AbstractDirectory
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: AbstractDirectory.java,v 1.0 Nov 11, 2009 5:31:04 PM haraldk Exp$
|
||||
*/
|
||||
public abstract class AbstractDirectory implements Directory {
|
||||
// A linked hashmap or a stable bag structure might also work..
|
||||
private final List<Entry> mEntries = new ArrayList<Entry>();
|
||||
|
||||
protected AbstractDirectory(final Collection<? extends Entry> pEntries) {
|
||||
if (pEntries != null) {
|
||||
mEntries.addAll(pEntries);
|
||||
}
|
||||
}
|
||||
|
||||
public Entry getEntryById(final Object pIdentifier) {
|
||||
for (Entry entry : this) {
|
||||
if (entry.getIdentifier().equals(pIdentifier)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Entry getEntryByName(final String pName) {
|
||||
for (Entry entry : this) {
|
||||
if (entry.getFieldName().equals(pName)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Iterator<Entry> iterator() {
|
||||
return mEntries.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws {@code UnsupportedOperationException} if this directory is read-only.
|
||||
*
|
||||
* @throws UnsupportedOperationException if this directory is read-only.
|
||||
* @see #isReadOnly()
|
||||
*/
|
||||
protected final void assertMutable() {
|
||||
if (isReadOnly()) {
|
||||
throw new UnsupportedOperationException("Directory is read-only");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean add(final Entry pEntry) {
|
||||
assertMutable();
|
||||
|
||||
// TODO: Replace if entry is already present?
|
||||
// Some directories may need special ordering, or may/may not support multiple entries for certain ids...
|
||||
return mEntries.add(pEntry);
|
||||
}
|
||||
|
||||
public boolean remove(final Object pEntry) {
|
||||
assertMutable();
|
||||
|
||||
return mEntries.remove(pEntry);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return mEntries.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation returns {@code true}.
|
||||
* Subclasses should override this method, if the directory is mutable.
|
||||
*
|
||||
* @return {@code true}
|
||||
*/
|
||||
public boolean isReadOnly() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Standard object support
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mEntries.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object pOther) {
|
||||
if (this == pOther) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (getClass() != pOther.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Safe cast, as it must be a subclass for the classes to be equal
|
||||
AbstractDirectory other = (AbstractDirectory) pOther;
|
||||
|
||||
return mEntries.equals(other.mEntries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s%s", getClass().getSimpleName(), mEntries.toString());
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package com.twelvemonkeys.imageio.metadata;
|
||||
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
|
||||
/**
|
||||
* AbstractEntry
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: AbstractEntry.java,v 1.0 Nov 12, 2009 12:43:13 AM haraldk Exp$
|
||||
*/
|
||||
public abstract class AbstractEntry implements Entry {
|
||||
|
||||
private final Object mIdentifier;
|
||||
private final Object mValue; // TODO: Might need to be mutable..
|
||||
|
||||
protected AbstractEntry(final Object pIdentifier, final Object pValue) {
|
||||
Validate.notNull(pIdentifier, "identifier");
|
||||
|
||||
mIdentifier = pIdentifier;
|
||||
mValue = pValue;
|
||||
}
|
||||
|
||||
public Object getIdentifier() {
|
||||
return mIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code null}, meaning unknown or undefined.
|
||||
*
|
||||
* @return {@code null}.
|
||||
*/
|
||||
public String getFieldName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public Object getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public String getValueAsString() {
|
||||
return String.valueOf(mValue);
|
||||
}
|
||||
|
||||
public String getTypeName() {
|
||||
if (mValue == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mValue.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
public int valueCount() {
|
||||
// TODO: Collection support?
|
||||
if (mValue != null && mValue.getClass().isArray()) {
|
||||
return Array.getLength(mValue);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/// Object
|
||||
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mIdentifier.hashCode() + 31 * mValue.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object pOther) {
|
||||
if (this == pOther) {
|
||||
return true;
|
||||
}
|
||||
if (!(pOther instanceof AbstractEntry)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AbstractEntry other = (AbstractEntry) pOther;
|
||||
|
||||
return mIdentifier.equals(other.mIdentifier) && (
|
||||
mValue == null && other.mValue == null || mValue != null && mValue.equals(other.mValue)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String name = getFieldName();
|
||||
String nameStr = name != null ? "/" + name + "" : "";
|
||||
|
||||
String type = getTypeName();
|
||||
String typeStr = type != null ? " (" + type + ")" : "";
|
||||
|
||||
return String.format("%s%s: %s%s", getIdentifier(), nameStr, getValueAsString(), typeStr);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.twelvemonkeys.imageio.metadata;
|
||||
|
||||
/**
|
||||
* Directory
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: Directory.java,v 1.0 Nov 11, 2009 4:20:58 PM haraldk Exp$
|
||||
*/
|
||||
public interface Directory extends Iterable<Entry> {
|
||||
// TODO: Spec when more entries exist? Or make Entry support multi-values!?
|
||||
// For multiple entries with same id in directory, the first entry (using the order from the stream) will be returned
|
||||
Entry getEntryById(Object pIdentifier);
|
||||
|
||||
Entry getEntryByName(String pName);
|
||||
|
||||
// Iterator containing the entries in
|
||||
//Iterator<Entry> getBestEntries(Object pIdentifier, Object pQualifier, String pLanguage);
|
||||
|
||||
|
||||
/// Collection-like API
|
||||
// TODO: addOrReplaceIfPresent... (trouble for multi-values) Or mutable entries?
|
||||
// boolean replace(Entry pEntry)??
|
||||
// boolean contains(Object pIdentifier)?
|
||||
|
||||
boolean add(Entry pEntry);
|
||||
|
||||
boolean remove(Object pEntry); // Object in case we retro-fit Collection/Map..
|
||||
|
||||
int size();
|
||||
|
||||
boolean isReadOnly();
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.twelvemonkeys.imageio.metadata;
|
||||
|
||||
/**
|
||||
* Entry
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: Entry.java,v 1.0 Nov 11, 2009 4:21:08 PM haraldk Exp$
|
||||
*/
|
||||
public interface Entry {
|
||||
// "tag" identifier from spec
|
||||
Object getIdentifier();
|
||||
|
||||
// Human readable "tag" (field) name from sepc
|
||||
String getFieldName();
|
||||
|
||||
// The internal "tag" value as stored in the stream, may be a Directory
|
||||
Object getValue();
|
||||
|
||||
// Human readable "tag" value
|
||||
String getValueAsString();
|
||||
|
||||
//void setValue(Object pValue); // TODO: qualifiers...
|
||||
|
||||
// Optional, implementation/spec specific type, describing the object returned from getValue
|
||||
String getTypeName();
|
||||
|
||||
// TODO: Or something like getValue(qualifierType, qualifierValue) + getQualifiers()/getQualifierValues
|
||||
// TODO: The problem with current model is getEntry() which only has single value support
|
||||
|
||||
// Optional, xml:lang-support
|
||||
//String getLanguage();
|
||||
|
||||
// Optional, XMP alt-support. TODO: Do we need both?
|
||||
//Object getQualifier();
|
||||
|
||||
// For arrays only
|
||||
int valueCount();
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.twelvemonkeys.imageio.metadata;
|
||||
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* MetadataReader
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: MetadataReader.java,v 1.0 Nov 13, 2009 8:38:11 PM haraldk Exp$
|
||||
*/
|
||||
public abstract class MetadataReader {
|
||||
public abstract Directory read(ImageInputStream pInput) throws IOException;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.twelvemonkeys.imageio.metadata.exif;
|
||||
|
||||
/**
|
||||
* EXIF
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: EXIF.java,v 1.0 Nov 11, 2009 5:36:04 PM haraldk Exp$
|
||||
*/
|
||||
interface EXIF {
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
static int EXIF_IFD = 0x8769;
|
||||
|
||||
static String[] TYPE_NAMES = {
|
||||
"BYTE", "ASCII", "SHORT", "LONG", "RATIONAL",
|
||||
|
||||
"SBYTE", "UNDEFINED", "SSHORT", "SLONG", "SRATIONAL", "FLOAT", "DOUBLE",
|
||||
};
|
||||
|
||||
static int[] TYPE_LENGTHS = {
|
||||
1, 1, 2, 4, 8,
|
||||
|
||||
1, 1, 2, 4, 8, 4, 8,
|
||||
};
|
||||
|
||||
int TIFF_MAGIC = 42;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.twelvemonkeys.imageio.metadata.exif;
|
||||
|
||||
import com.twelvemonkeys.imageio.metadata.AbstractDirectory;
|
||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* EXIFDirectory
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: EXIFDirectory.java,v 1.0 Nov 11, 2009 5:02:59 PM haraldk Exp$
|
||||
*/
|
||||
final class EXIFDirectory extends AbstractDirectory {
|
||||
EXIFDirectory(final Collection<? extends Entry> pEntries) {
|
||||
super(pEntries);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.twelvemonkeys.imageio.metadata.exif;
|
||||
|
||||
import com.twelvemonkeys.imageio.metadata.AbstractEntry;
|
||||
|
||||
/**
|
||||
* EXIFEntry
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: EXIFEntry.java,v 1.0 Nov 13, 2009 5:47:35 PM haraldk Exp$
|
||||
*/
|
||||
final class EXIFEntry extends AbstractEntry {
|
||||
final private short mType;
|
||||
|
||||
EXIFEntry(final Object pIdentifier, final Object pValue, final short pType) {
|
||||
super(pIdentifier, pValue);
|
||||
mType = pType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFieldName() {
|
||||
// TODO: Need tons of constants... ;-)
|
||||
return super.getFieldName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
return EXIF.TYPE_NAMES[mType];
|
||||
}
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
package com.twelvemonkeys.imageio.metadata.exif;
|
||||
|
||||
import com.twelvemonkeys.imageio.metadata.Directory;
|
||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
import com.twelvemonkeys.imageio.metadata.MetadataReader;
|
||||
import com.twelvemonkeys.lang.StringUtil;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* EXIFReader
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: EXIFReader.java,v 1.0 Nov 13, 2009 5:42:51 PM haraldk Exp$
|
||||
*/
|
||||
public final class EXIFReader extends MetadataReader {
|
||||
|
||||
@Override
|
||||
public Directory read(final ImageInputStream pInput) throws IOException {
|
||||
byte[] bom = new byte[2];
|
||||
pInput.readFully(bom);
|
||||
if (bom[0] == 'I' && bom[1] == 'I') {
|
||||
pInput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
else if (!(bom[0] == 'M' && bom[1] == 'M')) {
|
||||
throw new IIOException(String.format("Invalid byte order marker '%s'", StringUtil.decode(bom, 0, bom.length, "ASCII")));
|
||||
}
|
||||
|
||||
int magic = pInput.readUnsignedShort();
|
||||
if (magic != EXIF.TIFF_MAGIC) {
|
||||
throw new IIOException(String.format("Wrong TIFF magic in EXIF data: %04x, expected: %04x", magic, EXIF.TIFF_MAGIC));
|
||||
}
|
||||
|
||||
long directoryOffset = pInput.readUnsignedInt();
|
||||
|
||||
return readDirectory(pInput, directoryOffset);
|
||||
}
|
||||
|
||||
private EXIFDirectory readDirectory(final ImageInputStream pInput, final long pOffset) throws IOException {
|
||||
List<Entry> entries = new ArrayList<Entry>();
|
||||
|
||||
pInput.seek(pOffset);
|
||||
int entryCount = pInput.readUnsignedShort();
|
||||
|
||||
for (int i = 0; i < entryCount; i++) {
|
||||
entries.add(readEntry(pInput));
|
||||
}
|
||||
|
||||
long nextOffset = pInput.readUnsignedInt();
|
||||
|
||||
if (nextOffset != 0) {
|
||||
EXIFDirectory next = readDirectory(pInput, nextOffset);
|
||||
|
||||
for (Entry entry : next) {
|
||||
entries.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return new EXIFDirectory(entries);
|
||||
}
|
||||
|
||||
private EXIFEntry readEntry(final ImageInputStream pInput) throws IOException {
|
||||
int tagId = pInput.readUnsignedShort();
|
||||
|
||||
short type = pInput.readShort();
|
||||
int count = pInput.readInt(); // Number of values
|
||||
|
||||
Object value;
|
||||
|
||||
// TODO: Handle other sub-IFDs
|
||||
// GPS IFD: 0x8825, Interoperability IFD: 0xA005
|
||||
if (tagId == EXIF.EXIF_IFD) {
|
||||
long offset = pInput.readUnsignedInt();
|
||||
pInput.mark();
|
||||
|
||||
try {
|
||||
value = readDirectory(pInput, offset);
|
||||
}
|
||||
finally {
|
||||
pInput.reset();
|
||||
}
|
||||
}
|
||||
else {
|
||||
int valueLength = getValueLength(type, count);
|
||||
|
||||
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 = readValue(pInput, valueOffset, type, count);
|
||||
}
|
||||
}
|
||||
|
||||
return new EXIFEntry(tagId, value, type);
|
||||
}
|
||||
|
||||
private Object readValue(final ImageInputStream pInput, final long pOffset, final short pType, final int pCount) throws IOException {
|
||||
long pos = pInput.getStreamPosition();
|
||||
try {
|
||||
pInput.seek(pOffset);
|
||||
return readValueInLine(pInput, pType, pCount);
|
||||
}
|
||||
finally {
|
||||
pInput.seek(pos);
|
||||
}
|
||||
}
|
||||
|
||||
private Object readValueInLine(final ImageInputStream pInput, final short pType, final int pCount) throws IOException {
|
||||
return readValueDirect(pInput, pType, pCount);
|
||||
}
|
||||
|
||||
private static Object readValueDirect(final ImageInputStream pInput, final short pType, final int pCount) throws IOException {
|
||||
switch (pType) {
|
||||
case 2:
|
||||
// TODO: This might be UTF-8 or ISO-8859-1, even spec says ASCII
|
||||
byte[] ascii = new byte[pCount];
|
||||
pInput.readFully(ascii);
|
||||
return StringUtil.decode(ascii, 0, ascii.length, "UTF-8"); // UTF-8 is ASCII compatible
|
||||
case 1:
|
||||
if (pCount == 1) {
|
||||
return pInput.readUnsignedByte();
|
||||
}
|
||||
case 6:
|
||||
if (pCount == 1) {
|
||||
return pInput.readByte();
|
||||
}
|
||||
case 7:
|
||||
byte[] bytes = new byte[pCount];
|
||||
pInput.readFully(bytes);
|
||||
return bytes;
|
||||
case 3:
|
||||
if (pCount == 1) {
|
||||
return pInput.readUnsignedShort();
|
||||
}
|
||||
case 8:
|
||||
if (pCount == 1) {
|
||||
return pInput.readShort();
|
||||
}
|
||||
|
||||
short[] shorts = new short[pCount];
|
||||
pInput.readFully(shorts, 0, shorts.length);
|
||||
return shorts;
|
||||
case 4:
|
||||
if (pCount == 1) {
|
||||
return pInput.readUnsignedInt();
|
||||
}
|
||||
case 9:
|
||||
if (pCount == 1) {
|
||||
return pInput.readInt();
|
||||
}
|
||||
|
||||
int[] ints = new int[pCount];
|
||||
pInput.readFully(ints, 0, ints.length);
|
||||
return ints;
|
||||
case 11:
|
||||
if (pCount == 1) {
|
||||
return pInput.readFloat();
|
||||
}
|
||||
|
||||
float[] floats = new float[pCount];
|
||||
pInput.readFully(floats, 0, floats.length);
|
||||
return floats;
|
||||
case 12:
|
||||
if (pCount == 1) {
|
||||
return pInput.readDouble();
|
||||
}
|
||||
|
||||
double[] doubles = new double[pCount];
|
||||
pInput.readFully(doubles, 0, doubles.length);
|
||||
return doubles;
|
||||
|
||||
// TODO: Consider using a Rational class
|
||||
case 5:
|
||||
if (pCount == 1) {
|
||||
return pInput.readUnsignedInt() / (double) pInput.readUnsignedInt();
|
||||
}
|
||||
|
||||
double[] rationals = new double[pCount];
|
||||
for (int i = 0; i < rationals.length; i++) {
|
||||
rationals[i] = pInput.readUnsignedInt() / (double) pInput.readUnsignedInt();
|
||||
}
|
||||
|
||||
return rationals;
|
||||
case 10:
|
||||
if (pCount == 1) {
|
||||
return pInput.readInt() / (double) pInput.readInt();
|
||||
}
|
||||
|
||||
double[] srationals = new double[pCount];
|
||||
for (int i = 0; i < srationals.length; i++) {
|
||||
srationals[i] = pInput.readInt() / (double) pInput.readInt();
|
||||
}
|
||||
|
||||
return srationals;
|
||||
|
||||
default:
|
||||
throw new IIOException(String.format("Unknown EXIF type '%s'", pType));
|
||||
}
|
||||
}
|
||||
|
||||
private int getValueLength(final int pType, final int pCount) {
|
||||
if (pType > 0 && pType <= EXIF.TYPE_LENGTHS.length) {
|
||||
return EXIF.TYPE_LENGTHS[pType - 1] * pCount;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package com.twelvemonkeys.imageio.metadata.iptc;
|
||||
|
||||
/**
|
||||
* IPTC
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: IPTC.java,v 1.0 Nov 11, 2009 6:20:21 PM haraldk Exp$
|
||||
*/
|
||||
public interface IPTC {
|
||||
static final int ENVELOPE_RECORD = 1 << 8;
|
||||
static final int APPLICATION_RECORD = 2 << 8;
|
||||
|
||||
static final int TAG_CODED_CHARACTER_SET = ENVELOPE_RECORD | 90;
|
||||
|
||||
/** 2:00 Record Version (mandatory) */
|
||||
public static final int TAG_RECORD_VERSION = APPLICATION_RECORD; // 0x0200
|
||||
|
||||
/** 2:03 Object Type Reference */
|
||||
public static final int TAG_OBJECT_TYPE_REFERENCE = APPLICATION_RECORD | 3;
|
||||
/** 2:04 Object Attribute Reference (repeatable) */
|
||||
public static final int TAG_OBJECT_ATTRIBUTE_REFERENCE = APPLICATION_RECORD | 4;
|
||||
/** 2:05 Object Name */
|
||||
public static final int TAG_OBJECT_NAME = APPLICATION_RECORD | 5; // 0x0205
|
||||
/** 2:07 Edit Status */
|
||||
public static final int TAG_EDIT_STATUS = APPLICATION_RECORD | 7;
|
||||
/** 2:08 Editorial Update */
|
||||
public static final int TAG_EDITORIAL_UPDATE = APPLICATION_RECORD | 8;
|
||||
/** 2:10 Urgency */
|
||||
public static final int TAG_URGENCY = APPLICATION_RECORD | 10;
|
||||
/** 2:12 Subect Reference (repeatable) */
|
||||
public static final int TAG_SUBJECT_REFERENCE = APPLICATION_RECORD | 12;
|
||||
/** 2:15 Category */
|
||||
public static final int TAG_CATEGORY = APPLICATION_RECORD | 15; // 0x020f
|
||||
/** 2:20 Supplemental Category (repeatable) */
|
||||
public static final int TAG_SUPPLEMENTAL_CATEGORIES = APPLICATION_RECORD | 20;
|
||||
/** 2:22 Fixture Identifier */
|
||||
public static final int TAG_FIXTURE_IDENTIFIER = APPLICATION_RECORD | 22;
|
||||
/** 2:25 Keywords (repeatable) */
|
||||
public static final int TAG_KEYWORDS = APPLICATION_RECORD | 25;
|
||||
/** 2:26 Content Locataion Code (repeatable) */
|
||||
public static final int TAG_CONTENT_LOCATION_CODE = APPLICATION_RECORD | 26;
|
||||
/** 2:27 Content Locataion Name (repeatable) */
|
||||
public static final int TAG_CONTENT_LOCATION_NAME = APPLICATION_RECORD | 27;
|
||||
/** 2:30 Release Date */
|
||||
public static final int TAG_RELEASE_DATE = APPLICATION_RECORD | 30;
|
||||
/** 2:35 Release Time */
|
||||
public static final int TAG_RELEASE_TIME = APPLICATION_RECORD | 35;
|
||||
/** 2:37 Expiration Date */
|
||||
public static final int TAG_EXPIRATION_DATE = APPLICATION_RECORD | 37;
|
||||
/** 2:38 Expiration Time */
|
||||
public static final int TAG_EXPIRATION_TIME = APPLICATION_RECORD | 38;
|
||||
/** 2:40 Special Instructions */
|
||||
public static final int TAG_SPECIAL_INSTRUCTIONS = APPLICATION_RECORD | 40; // 0x0228
|
||||
/** 2:42 Action Advised (1: Kill, 2: Replace, 3: Append, 4: Reference) */
|
||||
public static final int TAG_ACTION_ADVICED = APPLICATION_RECORD | 42;
|
||||
/** 2:45 Reference Service (repeatable in triplets with 2:47 and 2:50) */
|
||||
public static final int TAG_REFERENCE_SERVICE = APPLICATION_RECORD | 45;
|
||||
/** 2:47 Reference Date (mandatory if 2:45 present) */
|
||||
public static final int TAG_REFERENCE_DATE = APPLICATION_RECORD | 47;
|
||||
/** 2:50 Reference Number (mandatory if 2:45 present) */
|
||||
public static final int TAG_REFERENCE_NUMBER = APPLICATION_RECORD | 50;
|
||||
/** 2:55 Date Created */
|
||||
public static final int TAG_DATE_CREATED = APPLICATION_RECORD | 55; // 0x0237
|
||||
/** 2:60 Time Created */
|
||||
public static final int TAG_TIME_CREATED = APPLICATION_RECORD | 60;
|
||||
/** 2:62 Digital Creation Date */
|
||||
public static final int TAG_DIGITAL_CREATION_DATE = APPLICATION_RECORD | 62;
|
||||
/** 2:63 Digital Creation Date */
|
||||
public static final int TAG_DIGITAL_CREATION_TIME = APPLICATION_RECORD | 63;
|
||||
/** 2:65 Originating Program */
|
||||
public static final int TAG_ORIGINATING_PROGRAM = APPLICATION_RECORD | 65;
|
||||
/** 2:70 Program Version (only valid if 2:65 present) */
|
||||
public static final int TAG_PROGRAM_VERSION = APPLICATION_RECORD | 70;
|
||||
/** 2:75 Object Cycle (a: morning, p: evening, b: both) */
|
||||
public static final int TAG_OBJECT_CYCLE = APPLICATION_RECORD | 75;
|
||||
/** 2:80 By-line (repeatable) */
|
||||
public static final int TAG_BY_LINE = APPLICATION_RECORD | 80; // 0x0250
|
||||
/** 2:85 By-line Title (repeatable) */
|
||||
public static final int TAG_BY_LINE_TITLE = APPLICATION_RECORD | 85; // 0x0255
|
||||
/** 2:90 City */
|
||||
public static final int TAG_CITY = APPLICATION_RECORD | 90; // 0x025a
|
||||
/** 2:92 Sub-location */
|
||||
public static final int TAG_SUB_LOCATION = APPLICATION_RECORD | 92;
|
||||
/** 2:95 Province/State */
|
||||
public static final int TAG_PROVINCE_OR_STATE = APPLICATION_RECORD | 95; // 0x025f
|
||||
/** 2:100 Country/Primary Location Code */
|
||||
public static final int TAG_COUNTRY_OR_PRIMARY_LOCATION_CODE = APPLICATION_RECORD | 100;
|
||||
/** 2:101 Country/Primary Location Name */
|
||||
public static final int TAG_COUNTRY_OR_PRIMARY_LOCATION = APPLICATION_RECORD | 101; // 0x0265
|
||||
/** 2:103 Original Transmission Reference */
|
||||
public static final int TAG_ORIGINAL_TRANSMISSION_REFERENCE = APPLICATION_RECORD | 103; // 0x0267
|
||||
/** 2:105 Headline */
|
||||
public static final int TAG_HEADLINE = APPLICATION_RECORD | 105; // 0x0269
|
||||
/** 2:110 Credit */
|
||||
public static final int TAG_CREDIT = APPLICATION_RECORD | 110; // 0x026e
|
||||
/** 2:115 Source */
|
||||
public static final int TAG_SOURCE = APPLICATION_RECORD | 115; // 0x0273
|
||||
/** 2:116 Copyright Notice */
|
||||
public static final int TAG_COPYRIGHT_NOTICE = APPLICATION_RECORD | 116; // 0x0274
|
||||
/** 2:118 Contact */
|
||||
public static final int TAG_CONTACT = APPLICATION_RECORD | 118;
|
||||
/** 2:120 Catption/Abstract */
|
||||
public static final int TAG_CAPTION = APPLICATION_RECORD | 120; // 0x0278
|
||||
/** 2:122 Writer/Editor (repeatable) */
|
||||
public static final int TAG_WRITER = APPLICATION_RECORD | 122; // 0x027a
|
||||
/** 2:125 Rasterized Caption (binary data) */
|
||||
public static final int TAG_RASTERIZED_CATPTION = APPLICATION_RECORD | 125;
|
||||
/** 2:130 Image Type */
|
||||
public static final int TAG_IMAGE_TYPE = APPLICATION_RECORD | 130;
|
||||
/** 2:131 Image Orientation */
|
||||
public static final int TAG_IMAGE_ORIENTATION = APPLICATION_RECORD | 131;
|
||||
/** 2:135 Language Identifier */
|
||||
public static final int TAG_LANGUAGE_IDENTIFIER = APPLICATION_RECORD | 135;
|
||||
|
||||
// TODO: 2:150-2:154 Audio
|
||||
|
||||
// TODO: Should we expose this field?
|
||||
/**
|
||||
* 2:199 JobMinder Assignment Data (Custom IPTC field).
|
||||
* A common custom IPTC field used by a now discontinued application called JobMinder.
|
||||
*
|
||||
* @see <a href="http://www.jobminder.net/">JobMinder Homepage</a>
|
||||
*/
|
||||
static final int CUSTOM_TAG_JOBMINDER_ASSIGNMENT_DATA = APPLICATION_RECORD | 199;
|
||||
|
||||
// TODO: Other custom fields in 155-200 range?
|
||||
|
||||
// TODO: 2:200-2:202 Object Preview Data
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.twelvemonkeys.imageio.metadata.iptc;
|
||||
|
||||
import com.twelvemonkeys.imageio.metadata.AbstractDirectory;
|
||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* IPTCDirectory
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: IPTCDirectory.java,v 1.0 Nov 11, 2009 5:02:59 PM haraldk Exp$
|
||||
*/
|
||||
final class IPTCDirectory extends AbstractDirectory {
|
||||
IPTCDirectory(final Collection<? extends Entry> pEntries) {
|
||||
super(pEntries);
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.twelvemonkeys.imageio.metadata.iptc;
|
||||
|
||||
import com.twelvemonkeys.imageio.metadata.AbstractEntry;
|
||||
|
||||
/**
|
||||
* IPTCEntry
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: IPTCEntry.java,v 1.0 Nov 13, 2009 8:57:04 PM haraldk Exp$
|
||||
*/
|
||||
class IPTCEntry extends AbstractEntry {
|
||||
public IPTCEntry(int pTagId, Object pValue) {
|
||||
super(pTagId, pValue);
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package com.twelvemonkeys.imageio.metadata.iptc;
|
||||
|
||||
import com.twelvemonkeys.imageio.metadata.Directory;
|
||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
import com.twelvemonkeys.imageio.metadata.MetadataReader;
|
||||
import com.twelvemonkeys.lang.StringUtil;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IPTCReader
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: IPTCReader.java,v 1.0 Nov 13, 2009 8:37:23 PM haraldk Exp$
|
||||
*/
|
||||
public class IPTCReader extends MetadataReader {
|
||||
private static final int ENCODING_UNKNOWN = -1;
|
||||
private static final int ENCODING_UNSPECIFIED = 0;
|
||||
private static final int ENCODING_UTF_8 = 0x1b2547;
|
||||
|
||||
private int mEncoding = ENCODING_UNSPECIFIED;
|
||||
|
||||
|
||||
@Override
|
||||
public Directory read(final ImageInputStream pInput) throws IOException {
|
||||
final List<Entry> entries = new ArrayList<Entry>();
|
||||
|
||||
// 0x1c identifies start of a tag
|
||||
while (pInput.read() == 0x1c) {
|
||||
int tagId = pInput.readShort();
|
||||
int tagByteCount = pInput.readUnsignedShort();
|
||||
|
||||
Entry entry = readEntry(pInput, tagId, tagByteCount);
|
||||
if (entry != null) {
|
||||
entries.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return new IPTCDirectory(entries);
|
||||
}
|
||||
|
||||
private Entry readEntry(final ImageInputStream pInput, final int pTagId, final int pLength) throws IOException {
|
||||
Object value = null;
|
||||
|
||||
switch (pTagId) {
|
||||
case IPTC.TAG_CODED_CHARACTER_SET:
|
||||
// TODO: Mapping from ISO 646 to Java supported character sets?
|
||||
// TODO: Move somewhere else?
|
||||
mEncoding = parseEncoding(pInput, pLength);
|
||||
return null;
|
||||
case IPTC.TAG_RECORD_VERSION:
|
||||
// A single unsigned short value
|
||||
value = pInput.readUnsignedShort();
|
||||
break;
|
||||
// case IPTC.TAG_RELEASE_DATE:
|
||||
// case IPTC.TAG_EXPIRATION_DATE:
|
||||
// case IPTC.TAG_REFERENCE_DATE:
|
||||
// case IPTC.TAG_DATE_CREATED:
|
||||
// case IPTC.TAG_DIGITAL_CREATION_DATE:
|
||||
// // Date object
|
||||
// Date date = parseISO8601DatePart(pInput, tagByteCount);
|
||||
// if (date != null) {
|
||||
// directory.setDate(tagIdentifier, date);
|
||||
// return;
|
||||
// }
|
||||
// case IPTC.TAG_RELEASE_TIME:
|
||||
// case IPTC.TAG_EXPIRATION_TIME:
|
||||
// case IPTC.TAG_TIME_CREATED:
|
||||
// case IPTC.TAG_DIGITAL_CREATION_TIME:
|
||||
// // NOTE: Spec says fields should be sent in order, so this is okay
|
||||
// date = getDateForTime(directory, tagIdentifier);
|
||||
//
|
||||
// Date time = parseISO8601TimePart(pInput, tagByteCount, date);
|
||||
// if (time != null) {
|
||||
// directory.setDate(tagIdentifier, time);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
default:
|
||||
// Skip non-Application fields, as they are typically not human readable
|
||||
if ((pTagId & 0xff00) != IPTC.APPLICATION_RECORD) {
|
||||
pInput.skipBytes(pLength);
|
||||
return null;
|
||||
}
|
||||
|
||||
// fall through
|
||||
}
|
||||
|
||||
// If we don't have a value, treat it as a string
|
||||
if (value == null) {
|
||||
if (pLength < 1) {
|
||||
value = "(No value)";
|
||||
}
|
||||
else {
|
||||
value = parseString(pInput, pLength);
|
||||
}
|
||||
}
|
||||
|
||||
return new IPTCEntry(pTagId, value);
|
||||
}
|
||||
|
||||
private int parseEncoding(final ImageInputStream pInput, int tagByteCount) throws IOException {
|
||||
return tagByteCount == 3
|
||||
&& (pInput.readUnsignedByte() << 16 | pInput.readUnsignedByte() << 8 | pInput.readUnsignedByte()) == ENCODING_UTF_8
|
||||
? ENCODING_UTF_8 : ENCODING_UNKNOWN;
|
||||
}
|
||||
|
||||
// TODO: Pass encoding as parameter? Use if specified
|
||||
private String parseString(final ImageInputStream pInput, final int pLength) throws IOException {
|
||||
byte[] data = new byte[pLength];
|
||||
pInput.readFully(data);
|
||||
|
||||
// NOTE: The IPTC specification says character data should use ISO 646 or ISO 2022 encoding.
|
||||
// UTF-8 contains all 646 characters, but not 2022.
|
||||
// This is however close to what libiptcdata does, see: http://libiptcdata.sourceforge.net/docs/iptc-i18n.html
|
||||
Charset charset = Charset.forName("UTF-8");
|
||||
CharsetDecoder decoder = charset.newDecoder();
|
||||
|
||||
try {
|
||||
// First try to decode using UTF-8 (which seems to be the de-facto standard)
|
||||
// Will fail fast on illegal UTF-8-sequences
|
||||
CharBuffer chars = decoder.onMalformedInput(CodingErrorAction.REPORT)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPORT)
|
||||
.decode(ByteBuffer.wrap(data));
|
||||
return chars.toString();
|
||||
}
|
||||
catch (CharacterCodingException notUTF8) {
|
||||
if (mEncoding == ENCODING_UTF_8) {
|
||||
throw new IIOException("Wrong encoding of IPTC data, explicitly set to UTF-8 in DataSet 1:90", notUTF8);
|
||||
}
|
||||
|
||||
// Fall back to use ISO-8859-1
|
||||
// This will not fail, but may may create wrong fallback-characters
|
||||
return StringUtil.decode(data, 0, data.length, "ISO8859_1");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package com.twelvemonkeys.imageio.metadata.xmp;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.UnsupportedCharsetException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* XMPScannerTestCase
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: XMPScannerTestCase.java,v 1.0 Nov 13, 2009 3:59:43 PM haraldk Exp$
|
||||
*/
|
||||
public class XMPScannerTestCase extends TestCase {
|
||||
|
||||
static final String XMP =
|
||||
"<?xpacket begin=\"\uFEFF\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>" +
|
||||
"<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"Adobe XMP Core 4.1-c036 46.276720, Fri Nov 13 2009 15:59:43 \">\n"+
|
||||
" <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n"+
|
||||
" <rdf:Description rdf:about=\"\"\n"+
|
||||
" xmlns:photoshop=\"http://ns.adobe.com/photoshop/1.0/\">\n"+
|
||||
" <photoshop:Source>twelvemonkeys.com</photoshop:Source>\n"+
|
||||
" </rdf:Description>\n"+
|
||||
" <rdf:Description rdf:about=\"\"\n"+
|
||||
" xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n"+
|
||||
" <dc:format>application/vnd.adobe.photoshop</dc:format>\n"+
|
||||
" </rdf:Description>\n"+
|
||||
" </rdf:RDF>\n"+
|
||||
"</x:xmpmeta>" +
|
||||
"<?xpacket end=\"w\"?>";
|
||||
|
||||
final Random mRandom = new Random(4934638567l);
|
||||
|
||||
private InputStream createRandomStream(final int pLength) {
|
||||
byte[] bytes = new byte[pLength];
|
||||
mRandom.nextBytes(bytes);
|
||||
return new ByteArrayInputStream(bytes);
|
||||
}
|
||||
|
||||
private InputStream createXMPStream(final String pXMP, final String pCharsetName) {
|
||||
try {
|
||||
return new SequenceInputStream(
|
||||
Collections.enumeration(
|
||||
Arrays.asList(
|
||||
createRandomStream(79),
|
||||
new ByteArrayInputStream(pXMP.getBytes(pCharsetName)),
|
||||
createRandomStream(31)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
UnsupportedCharsetException uce = new UnsupportedCharsetException(pCharsetName);
|
||||
uce.initCause(e);
|
||||
throw uce;
|
||||
}
|
||||
}
|
||||
|
||||
public void testScanForUTF8() throws IOException {
|
||||
InputStream stream = createXMPStream(XMP, "UTF-8");
|
||||
|
||||
Reader reader = XMPScanner.scanForXMPPacket(stream);
|
||||
|
||||
assertNotNull(reader);
|
||||
}
|
||||
|
||||
public void testScanForUTF8singleQuote() throws IOException {
|
||||
InputStream stream = createXMPStream(XMP, "UTF-8".replace("\"", "'"));
|
||||
|
||||
Reader reader = XMPScanner.scanForXMPPacket(stream);
|
||||
|
||||
assertNotNull(reader);
|
||||
}
|
||||
|
||||
public void testScanForUTF16BE() throws IOException {
|
||||
InputStream stream = createXMPStream(XMP, "UTF-16BE");
|
||||
|
||||
Reader reader = XMPScanner.scanForXMPPacket(stream);
|
||||
|
||||
assertNotNull(reader);
|
||||
}
|
||||
|
||||
public void testScanForUTF16BEsingleQuote() throws IOException {
|
||||
InputStream stream = createXMPStream(XMP, "UTF-16BE".replace("\"", "'"));
|
||||
|
||||
Reader reader = XMPScanner.scanForXMPPacket(stream);
|
||||
|
||||
assertNotNull(reader);
|
||||
}
|
||||
|
||||
public void testScanForUTF16LE() throws IOException {
|
||||
InputStream stream = createXMPStream(XMP, "UTF-16LE");
|
||||
|
||||
Reader reader = XMPScanner.scanForXMPPacket(stream);
|
||||
|
||||
assertNotNull(reader);
|
||||
}
|
||||
|
||||
public void testScanForUTF16LEsingleQuote() throws IOException {
|
||||
InputStream stream = createXMPStream(XMP, "UTF-16LE".replace("\"", "'"));
|
||||
|
||||
Reader reader = XMPScanner.scanForXMPPacket(stream);
|
||||
|
||||
assertNotNull(reader);
|
||||
}
|
||||
|
||||
// TODO: Default Java installation on OS X don't seem to have UTF-32 installed. Hmmm..
|
||||
// public void testUTF32BE() throws IOException {
|
||||
// InputStream stream = createXMPStream("UTF-32BE");
|
||||
//
|
||||
// Reader reader = XMPScanner.scanForXMPPacket(stream);
|
||||
//
|
||||
// assertNotNull(reader);
|
||||
// }
|
||||
//
|
||||
// public void testUTF32LE() throws IOException {
|
||||
// InputStream stream = createXMPStream("UTF-32LE");
|
||||
//
|
||||
// Reader reader = XMPScanner.scanForXMPPacket(stream);
|
||||
//
|
||||
// assertNotNull(reader);
|
||||
// }
|
||||
}
|
||||
Reference in New Issue
Block a user