mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-03 11:35:29 -04:00
Work in progress for PSD metadata support:
- Implemented XMP Reader, Directory and Entry - More EXIF and IPTC changes - Cleaning up
This commit is contained in:
parent
d24c2c1b08
commit
64b21b83bb
@ -13,7 +13,6 @@ import java.util.List;
|
|||||||
* @version $Id: AbstractDirectory.java,v 1.0 Nov 11, 2009 5:31:04 PM haraldk Exp$
|
* @version $Id: AbstractDirectory.java,v 1.0 Nov 11, 2009 5:31:04 PM haraldk Exp$
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractDirectory implements Directory {
|
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>();
|
private final List<Entry> mEntries = new ArrayList<Entry>();
|
||||||
|
|
||||||
protected AbstractDirectory(final Collection<? extends Entry> pEntries) {
|
protected AbstractDirectory(final Collection<? extends Entry> pEntries) {
|
||||||
@ -32,9 +31,9 @@ public abstract class AbstractDirectory implements Directory {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Entry getEntryByName(final String pName) {
|
public Entry getEntryByFieldName(final String pFieldName) {
|
||||||
for (Entry entry : this) {
|
for (Entry entry : this) {
|
||||||
if (entry.getFieldName().equals(pName)) {
|
if (entry.getFieldName() != null && entry.getFieldName().equals(pFieldName)) {
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,6 +65,7 @@ public abstract class AbstractDirectory implements Directory {
|
|||||||
return mEntries.add(pEntry);
|
return mEntries.add(pEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"SuspiciousMethodCalls"})
|
||||||
public boolean remove(final Object pEntry) {
|
public boolean remove(final Object pEntry) {
|
||||||
assertMutable();
|
assertMutable();
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ public abstract class AbstractEntry implements Entry {
|
|||||||
mValue = pValue;
|
mValue = pValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object getIdentifier() {
|
public final Object getIdentifier() {
|
||||||
return mIdentifier;
|
return mIdentifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ public interface Directory extends Iterable<Entry> {
|
|||||||
// For multiple entries with same id in directory, the first entry (using the order from the stream) will be returned
|
// 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 getEntryById(Object pIdentifier);
|
||||||
|
|
||||||
Entry getEntryByName(String pName);
|
Entry getEntryByFieldName(String pName);
|
||||||
|
|
||||||
// Iterator containing the entries in
|
// Iterator containing the entries in
|
||||||
//Iterator<Entry> getBestEntries(Object pIdentifier, Object pQualifier, String pLanguage);
|
//Iterator<Entry> getBestEntries(Object pIdentifier, Object pQualifier, String pLanguage);
|
||||||
|
@ -7,41 +7,8 @@ package com.twelvemonkeys.imageio.metadata.exif;
|
|||||||
* @author last modified by $Author: haraldk$
|
* @author last modified by $Author: haraldk$
|
||||||
* @version $Id: EXIF.java,v 1.0 Nov 11, 2009 5:36:04 PM haraldk Exp$
|
* @version $Id: EXIF.java,v 1.0 Nov 11, 2009 5:36:04 PM haraldk Exp$
|
||||||
*/
|
*/
|
||||||
interface EXIF {
|
public interface EXIF {
|
||||||
/*
|
int TAG_COLOR_SPACE = 40961;
|
||||||
1 = BYTE 8-bit unsigned integer.
|
int TAG_PIXEL_X_DIMENSION = 40962;
|
||||||
2 = ASCII 8-bit byte that contains a 7-bit ASCII code; the last byte
|
int TAG_PIXEL_Y_DIMENSION = 40963;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import com.twelvemonkeys.imageio.metadata.AbstractDirectory;
|
|||||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EXIFDirectory
|
* EXIFDirectory
|
||||||
|
@ -12,19 +12,42 @@ import com.twelvemonkeys.imageio.metadata.AbstractEntry;
|
|||||||
final class EXIFEntry extends AbstractEntry {
|
final class EXIFEntry extends AbstractEntry {
|
||||||
final private short mType;
|
final private short mType;
|
||||||
|
|
||||||
EXIFEntry(final Object pIdentifier, final Object pValue, final short pType) {
|
EXIFEntry(final int pIdentifier, final Object pValue, final short pType) {
|
||||||
super(pIdentifier, pValue);
|
super(pIdentifier, pValue);
|
||||||
|
|
||||||
|
if (pType < 1 || pType > TIFF.TYPE_NAMES.length) {
|
||||||
|
throw new IllegalArgumentException(String.format("Illegal EXIF type: %s", pType));
|
||||||
|
}
|
||||||
|
|
||||||
mType = pType;
|
mType = pType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFieldName() {
|
public String getFieldName() {
|
||||||
// TODO: Need tons of constants... ;-)
|
switch ((Integer) getIdentifier()) {
|
||||||
return super.getFieldName();
|
case TIFF.TAG_SOFTWARE:
|
||||||
|
return "Software";
|
||||||
|
case TIFF.TAG_DATE_TIME:
|
||||||
|
return "DateTime";
|
||||||
|
case TIFF.TAG_ARTIST:
|
||||||
|
return "Artist";
|
||||||
|
case TIFF.TAG_COPYRIGHT:
|
||||||
|
return "Copyright";
|
||||||
|
|
||||||
|
case EXIF.TAG_COLOR_SPACE:
|
||||||
|
return "ColorSpace";
|
||||||
|
case EXIF.TAG_PIXEL_X_DIMENSION:
|
||||||
|
return "PixelXDimension";
|
||||||
|
case EXIF.TAG_PIXEL_Y_DIMENSION:
|
||||||
|
return "PixelYDimension";
|
||||||
|
// TODO: More field names
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getTypeName() {
|
public String getTypeName() {
|
||||||
return EXIF.TYPE_NAMES[mType];
|
return TIFF.TYPE_NAMES[mType - 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,12 +29,12 @@ public final class EXIFReader extends MetadataReader {
|
|||||||
pInput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
|
pInput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
|
||||||
}
|
}
|
||||||
else if (!(bom[0] == 'M' && bom[1] == 'M')) {
|
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")));
|
throw new IIOException(String.format("Invalid TIFF byte order mark '%s', expected: 'II' or 'MM'", StringUtil.decode(bom, 0, bom.length, "ASCII")));
|
||||||
}
|
}
|
||||||
|
|
||||||
int magic = pInput.readUnsignedShort();
|
int magic = pInput.readUnsignedShort();
|
||||||
if (magic != EXIF.TIFF_MAGIC) {
|
if (magic != TIFF.TIFF_MAGIC) {
|
||||||
throw new IIOException(String.format("Wrong TIFF magic in EXIF data: %04x, expected: %04x", magic, EXIF.TIFF_MAGIC));
|
throw new IIOException(String.format("Wrong TIFF magic in EXIF data: %04x, expected: %04x", magic, TIFF.TIFF_MAGIC));
|
||||||
}
|
}
|
||||||
|
|
||||||
long directoryOffset = pInput.readUnsignedInt();
|
long directoryOffset = pInput.readUnsignedInt();
|
||||||
@ -73,9 +73,8 @@ public final class EXIFReader extends MetadataReader {
|
|||||||
|
|
||||||
Object value;
|
Object value;
|
||||||
|
|
||||||
// TODO: Handle other sub-IFDs
|
if (tagId == TIFF.IFD_EXIF || tagId == TIFF.IFD_GPS || tagId == TIFF.IFD_INTEROP) {
|
||||||
// GPS IFD: 0x8825, Interoperability IFD: 0xA005
|
// Parse sub IFDs
|
||||||
if (tagId == EXIF.EXIF_IFD) {
|
|
||||||
long offset = pInput.readUnsignedInt();
|
long offset = pInput.readUnsignedInt();
|
||||||
pInput.mark();
|
pInput.mark();
|
||||||
|
|
||||||
@ -207,8 +206,8 @@ public final class EXIFReader extends MetadataReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int getValueLength(final int pType, final int pCount) {
|
private int getValueLength(final int pType, final int pCount) {
|
||||||
if (pType > 0 && pType <= EXIF.TYPE_LENGTHS.length) {
|
if (pType > 0 && pType <= TIFF.TYPE_LENGTHS.length) {
|
||||||
return EXIF.TYPE_LENGTHS[pType - 1] * pCount;
|
return TIFF.TYPE_LENGTHS[pType - 1] * pCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
package com.twelvemonkeys.imageio.metadata.exif;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TIFF
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
|
* @author last modified by $Author: haraldk$
|
||||||
|
* @version $Id: TIFF.java,v 1.0 Nov 15, 2009 3:02:24 PM haraldk Exp$
|
||||||
|
*/
|
||||||
|
public interface TIFF {
|
||||||
|
int TIFF_MAGIC = 42;
|
||||||
|
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
String[] TYPE_NAMES = {
|
||||||
|
"BYTE", "ASCII", "SHORT", "LONG", "RATIONAL",
|
||||||
|
|
||||||
|
"SBYTE", "UNDEFINED", "SSHORT", "SLONG", "SRATIONAL", "FLOAT", "DOUBLE",
|
||||||
|
};
|
||||||
|
int[] TYPE_LENGTHS = {
|
||||||
|
1, 1, 2, 4, 8,
|
||||||
|
|
||||||
|
1, 1, 2, 4, 8, 4, 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
int IFD_EXIF = 0x8769;
|
||||||
|
int IFD_GPS = 0x8825;
|
||||||
|
int IFD_INTEROP = 0xA005;
|
||||||
|
|
||||||
|
|
||||||
|
int TAG_SOFTWARE = 305;
|
||||||
|
int TAG_DATE_TIME = 306;
|
||||||
|
int TAG_ARTIST = 315;
|
||||||
|
int TAG_COPYRIGHT = 33432;
|
||||||
|
}
|
@ -10,7 +10,18 @@ import com.twelvemonkeys.imageio.metadata.AbstractEntry;
|
|||||||
* @version $Id: IPTCEntry.java,v 1.0 Nov 13, 2009 8:57:04 PM haraldk Exp$
|
* @version $Id: IPTCEntry.java,v 1.0 Nov 13, 2009 8:57:04 PM haraldk Exp$
|
||||||
*/
|
*/
|
||||||
class IPTCEntry extends AbstractEntry {
|
class IPTCEntry extends AbstractEntry {
|
||||||
public IPTCEntry(int pTagId, Object pValue) {
|
public IPTCEntry(final int pTagId, final Object pValue) {
|
||||||
super(pTagId, pValue);
|
super(pTagId, pValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFieldName() {
|
||||||
|
switch ((Integer) getIdentifier()) {
|
||||||
|
case IPTC.TAG_SOURCE:
|
||||||
|
return "Source";
|
||||||
|
// TODO: More tags...
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ import java.util.List;
|
|||||||
* @author last modified by $Author: haraldk$
|
* @author last modified by $Author: haraldk$
|
||||||
* @version $Id: IPTCReader.java,v 1.0 Nov 13, 2009 8:37:23 PM haraldk Exp$
|
* @version $Id: IPTCReader.java,v 1.0 Nov 13, 2009 8:37:23 PM haraldk Exp$
|
||||||
*/
|
*/
|
||||||
public class IPTCReader extends MetadataReader {
|
public final class IPTCReader extends MetadataReader {
|
||||||
private static final int ENCODING_UNKNOWN = -1;
|
private static final int ENCODING_UNKNOWN = -1;
|
||||||
private static final int ENCODING_UNSPECIFIED = 0;
|
private static final int ENCODING_UNSPECIFIED = 0;
|
||||||
private static final int ENCODING_UTF_8 = 0x1b2547;
|
private static final int ENCODING_UTF_8 = 0x1b2547;
|
||||||
@ -38,10 +38,10 @@ public class IPTCReader extends MetadataReader {
|
|||||||
|
|
||||||
// 0x1c identifies start of a tag
|
// 0x1c identifies start of a tag
|
||||||
while (pInput.read() == 0x1c) {
|
while (pInput.read() == 0x1c) {
|
||||||
int tagId = pInput.readShort();
|
short tagId = pInput.readShort();
|
||||||
int tagByteCount = pInput.readUnsignedShort();
|
int tagByteCount = pInput.readUnsignedShort();
|
||||||
|
|
||||||
Entry entry = readEntry(pInput, tagId, tagByteCount);
|
Entry entry = readEntry(pInput, tagId, tagByteCount);
|
||||||
|
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
entries.add(entry);
|
entries.add(entry);
|
||||||
}
|
}
|
||||||
@ -50,7 +50,7 @@ public class IPTCReader extends MetadataReader {
|
|||||||
return new IPTCDirectory(entries);
|
return new IPTCDirectory(entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Entry readEntry(final ImageInputStream pInput, final int pTagId, final int pLength) throws IOException {
|
private IPTCEntry readEntry(final ImageInputStream pInput, final short pTagId, final int pLength) throws IOException {
|
||||||
Object value = null;
|
Object value = null;
|
||||||
|
|
||||||
switch (pTagId) {
|
switch (pTagId) {
|
||||||
@ -63,30 +63,6 @@ public class IPTCReader extends MetadataReader {
|
|||||||
// A single unsigned short value
|
// A single unsigned short value
|
||||||
value = pInput.readUnsignedShort();
|
value = pInput.readUnsignedShort();
|
||||||
break;
|
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:
|
default:
|
||||||
// Skip non-Application fields, as they are typically not human readable
|
// Skip non-Application fields, as they are typically not human readable
|
||||||
if ((pTagId & 0xff00) != IPTC.APPLICATION_RECORD) {
|
if ((pTagId & 0xff00) != IPTC.APPLICATION_RECORD) {
|
||||||
@ -100,7 +76,7 @@ public class IPTCReader extends MetadataReader {
|
|||||||
// If we don't have a value, treat it as a string
|
// If we don't have a value, treat it as a string
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
if (pLength < 1) {
|
if (pLength < 1) {
|
||||||
value = "(No value)";
|
value = null;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
value = parseString(pInput, pLength);
|
value = parseString(pInput, pLength);
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
package com.twelvemonkeys.imageio.metadata.xmp;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XMP
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
|
* @author last modified by $Author: haraldk$
|
||||||
|
* @version $Id: XMP.java,v 1.0 Nov 12, 2009 12:19:32 AM haraldk Exp$
|
||||||
|
*
|
||||||
|
* @see <a href="http://www.adobe.com/products/xmp/">Extensible Metadata Platform (XMP)</a>
|
||||||
|
*/
|
||||||
|
public interface XMP {
|
||||||
|
/** W3C Resource Description Format namespace */
|
||||||
|
String NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
|
||||||
|
|
||||||
|
/** Dublin Core Metadata Initiative namespace */
|
||||||
|
String NS_DC = "http://purl.org/dc/elements/1.1/";
|
||||||
|
|
||||||
|
String NS_EXIF = "http://ns.adobe.com/exif/1.0/";
|
||||||
|
|
||||||
|
String NS_PHOTOSHOP = "http://ns.adobe.com/photoshop/1.0/";
|
||||||
|
|
||||||
|
String NS_ST_REF = "http://ns.adobe.com/xap/1.0/sType/ResourceRef#";
|
||||||
|
|
||||||
|
String NS_TIFF = "http://ns.adobe.com/tiff/1.0/";
|
||||||
|
|
||||||
|
String NS_XAP = "http://ns.adobe.com/xap/1.0/";
|
||||||
|
|
||||||
|
String NS_XAP_MM = "http://ns.adobe.com/xap/1.0/mm/";
|
||||||
|
|
||||||
|
/** Contains the mapping from URI to default namespace prefix. */
|
||||||
|
Map<String, String> DEFAULT_NS_MAPPING = Collections.unmodifiableMap(new XMPNamespaceMapping());
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package com.twelvemonkeys.imageio.metadata.xmp;
|
||||||
|
|
||||||
|
import com.twelvemonkeys.imageio.metadata.AbstractDirectory;
|
||||||
|
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XMPDirectory
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
|
* @author last modified by $Author: haraldk$
|
||||||
|
* @version $Id: XMPDirectory.java,v 1.0 Nov 17, 2009 9:38:58 PM haraldk Exp$
|
||||||
|
*/
|
||||||
|
final class XMPDirectory extends AbstractDirectory {
|
||||||
|
// TODO: Store size of root directory, to allow serializing
|
||||||
|
// TODO: XMPDirectory, maybe not even an AbstractDirectory
|
||||||
|
// - Keeping the Document would allow for easier serialization
|
||||||
|
// TODO: Or use direct SAX parsing
|
||||||
|
public XMPDirectory(List<Entry> pEntries) {
|
||||||
|
super(pEntries);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package com.twelvemonkeys.imageio.metadata.xmp;
|
||||||
|
|
||||||
|
import com.twelvemonkeys.imageio.metadata.AbstractEntry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XMPEntry
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
|
* @author last modified by $Author: haraldk$
|
||||||
|
* @version $Id: XMPEntry.java,v 1.0 Nov 17, 2009 9:38:39 PM haraldk Exp$
|
||||||
|
*/
|
||||||
|
final class XMPEntry extends AbstractEntry {
|
||||||
|
private final String mFieldName;
|
||||||
|
|
||||||
|
public XMPEntry(final String pIdentifier, final Object pValue) {
|
||||||
|
this(pIdentifier, null, pValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public XMPEntry(final String pIdentifier, final String pFieldName, final Object pValue) {
|
||||||
|
super(pIdentifier, pValue);
|
||||||
|
mFieldName = pFieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"SuspiciousMethodCalls"})
|
||||||
|
@Override
|
||||||
|
public String getFieldName() {
|
||||||
|
return mFieldName != null ? mFieldName : XMP.DEFAULT_NS_MAPPING.get(getIdentifier());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package com.twelvemonkeys.imageio.metadata.xmp;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XMPNamespaceMapping
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
|
* @author last modified by $Author: haraldk$
|
||||||
|
* @version $Id: XMPNamespaceMapping.java,v 1.0 Nov 17, 2009 6:35:21 PM haraldk Exp$
|
||||||
|
*/
|
||||||
|
final class XMPNamespaceMapping extends HashMap<String, String> {
|
||||||
|
public XMPNamespaceMapping() {
|
||||||
|
put(XMP.NS_RDF, "rdf");
|
||||||
|
put(XMP.NS_DC, "dc");
|
||||||
|
put(XMP.NS_EXIF, "exif");
|
||||||
|
put(XMP.NS_PHOTOSHOP, "photoshop");
|
||||||
|
put(XMP.NS_ST_REF, "stRef");
|
||||||
|
put(XMP.NS_TIFF, "tiff");
|
||||||
|
put(XMP.NS_XAP, "xap");
|
||||||
|
put(XMP.NS_XAP_MM, "xapMM");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,195 @@
|
|||||||
|
package com.twelvemonkeys.imageio.metadata.xmp;
|
||||||
|
|
||||||
|
import com.twelvemonkeys.imageio.metadata.*;
|
||||||
|
import com.twelvemonkeys.imageio.util.IIOUtil;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.NamedNodeMap;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
import org.xml.sax.InputSource;
|
||||||
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
|
import javax.imageio.IIOException;
|
||||||
|
import javax.imageio.stream.ImageInputStream;
|
||||||
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XMPReader
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
|
* @author last modified by $Author: haraldk$
|
||||||
|
* @version $Id: XMPReader.java,v 1.0 Nov 14, 2009 11:04:30 PM haraldk Exp$
|
||||||
|
*/
|
||||||
|
public final class XMPReader extends MetadataReader {
|
||||||
|
@Override
|
||||||
|
public Directory read(final ImageInputStream pInput) throws IOException {
|
||||||
|
pInput.mark();
|
||||||
|
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(IIOUtil.createStreamAdapter(pInput), Charset.forName("UTF-8")));
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
System.out.println(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
pInput.reset();
|
||||||
|
|
||||||
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
|
factory.setNamespaceAware(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: Consider parsing using SAX?
|
||||||
|
// TODO: Determine encoding and parse using a Reader...
|
||||||
|
// TODO: Refactor scanner to return inputstream?
|
||||||
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||||
|
Document document = builder.parse(new InputSource(IIOUtil.createStreamAdapter(pInput)));
|
||||||
|
|
||||||
|
// XMLSerializer serializer = new XMLSerializer(System.err, System.getProperty("file.encoding"));
|
||||||
|
// serializer.serialize(document);
|
||||||
|
|
||||||
|
|
||||||
|
// Each rdf:Description is a Directory (but we can't really rely on that structure.. it's only convention)
|
||||||
|
// - Each element inside the rdf:Desc is an Entry
|
||||||
|
|
||||||
|
Node rdfRoot = document.getElementsByTagNameNS(XMP.NS_RDF, "RDF").item(0);
|
||||||
|
NodeList descriptions = document.getElementsByTagNameNS(XMP.NS_RDF, "Description");
|
||||||
|
|
||||||
|
return parseDirectories(rdfRoot, descriptions);
|
||||||
|
}
|
||||||
|
catch (SAXException e) {
|
||||||
|
throw new IIOException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
catch (ParserConfigurationException e) {
|
||||||
|
throw new RuntimeException(e); // TODO: Or IOException?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Consider using namespace-prefix in tags/identifiers and qName as field only!?
|
||||||
|
|
||||||
|
private XMPDirectory parseDirectories(final Node pParentNode, NodeList pNodes) {
|
||||||
|
Map<String, List<Entry>> subdirs = new LinkedHashMap<String, List<Entry>>();
|
||||||
|
|
||||||
|
for (Node desc : asIterable(pNodes)) {
|
||||||
|
if (desc.getParentNode() != pParentNode) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Node node : asIterable(desc.getChildNodes())) {
|
||||||
|
if (node.getNodeType() != Node.ELEMENT_NODE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup
|
||||||
|
List<Entry> dir = subdirs.get(node.getNamespaceURI());
|
||||||
|
if (dir == null) {
|
||||||
|
dir = new ArrayList<Entry>();
|
||||||
|
subdirs.put(node.getNamespaceURI(), dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object value;
|
||||||
|
|
||||||
|
Node parseType = node.getAttributes().getNamedItemNS(XMP.NS_RDF, "parseType");
|
||||||
|
if (parseType != null && "Resource".equals(parseType.getNodeValue())) {
|
||||||
|
List<Entry> entries = new ArrayList<Entry>();
|
||||||
|
|
||||||
|
for (Node child : asIterable(node.getChildNodes())) {
|
||||||
|
if (child.getNodeType() != Node.ELEMENT_NODE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Preserve the stRef namespace here..
|
||||||
|
entries.add(new XMPEntry(child.getNamespaceURI() + child.getLocalName(), child.getLocalName(), getChildTextValue(child)));
|
||||||
|
}
|
||||||
|
value = new XMPDirectory(entries);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// TODO: Support alternative RDF syntax (short-form), using attributes on desc
|
||||||
|
// NamedNodeMap attributes = node.getAttributes();
|
||||||
|
//
|
||||||
|
// for (Node attr : asIterable(attributes)) {
|
||||||
|
// System.out.println("attr.getNodeName(): " + attr.getNodeName());
|
||||||
|
// System.out.println("attr.getNodeValue(): " + attr.getNodeValue());
|
||||||
|
// }
|
||||||
|
|
||||||
|
value = getChildTextValue(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Preserve namespace (without URI?) here..
|
||||||
|
XMPEntry entry = new XMPEntry(node.getNamespaceURI() + node.getLocalName(), node.getLocalName(), value);
|
||||||
|
dir.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Entry> entries = new ArrayList<Entry>();
|
||||||
|
|
||||||
|
for (Map.Entry<String, List<Entry>> entry : subdirs.entrySet()) {
|
||||||
|
entries.add(new XMPEntry(entry.getKey(), new XMPDirectory(entry.getValue())));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new XMPDirectory(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object getChildTextValue(Node node) {
|
||||||
|
Object value;
|
||||||
|
Node child = node.getFirstChild();
|
||||||
|
|
||||||
|
String strVal = null;
|
||||||
|
if (child != null) {
|
||||||
|
strVal = child.getNodeValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
value = strVal != null ? strVal.trim() : "";
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Iterable<? extends Node> asIterable(final NamedNodeMap pNodeList) {
|
||||||
|
return new Iterable<Node>() {
|
||||||
|
public Iterator<Node> iterator() {
|
||||||
|
return new Iterator<Node>() {
|
||||||
|
private int mIndex;
|
||||||
|
|
||||||
|
public boolean hasNext() {
|
||||||
|
return pNodeList != null && pNodeList.getLength() > mIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node next() {
|
||||||
|
return pNodeList.item(mIndex++);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove() {
|
||||||
|
throw new UnsupportedOperationException("Method remove not supported");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Iterable<? extends Node> asIterable(final NodeList pNodeList) {
|
||||||
|
return new Iterable<Node>() {
|
||||||
|
public Iterator<Node> iterator() {
|
||||||
|
return new Iterator<Node>() {
|
||||||
|
private int mIndex;
|
||||||
|
|
||||||
|
public boolean hasNext() {
|
||||||
|
return pNodeList != null && pNodeList.getLength() > mIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Node next() {
|
||||||
|
return pNodeList.item(mIndex++);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove() {
|
||||||
|
throw new UnsupportedOperationException("Method remove not supported");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,215 @@
|
|||||||
|
package com.twelvemonkeys.imageio.metadata.xmp;
|
||||||
|
|
||||||
|
import com.twelvemonkeys.imageio.stream.BufferedImageInputStream;
|
||||||
|
import com.twelvemonkeys.imageio.util.IIOUtil;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import javax.imageio.stream.ImageInputStream;
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XMPScanner
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
|
* @author last modified by $Author: haraldk$
|
||||||
|
* @version $Id: XMPScanner.java,v 1.0 Nov 11, 2009 4:49:00 PM haraldk Exp$
|
||||||
|
*/
|
||||||
|
public final class XMPScanner {
|
||||||
|
/**
|
||||||
|
* {@code <?xpacket begin=}
|
||||||
|
* <p/>
|
||||||
|
* <ul>
|
||||||
|
* <li>
|
||||||
|
* 8-bit (UTF-8):
|
||||||
|
* 0x3C 0x3F 0x78 0x70 0x61 0x63 0x6B 0x65 0x74 0x20
|
||||||
|
* 0x62 0x65 0x67 0x69 0x6E 0x3D
|
||||||
|
* </li>
|
||||||
|
* <li>16-bit encoding (UCS-2, UTF-16): (either big- or little-endian order)
|
||||||
|
* 0x3C 0x00 0x3F 0x00 0x78 0x00 0x70 0x00 0x61 0x00
|
||||||
|
* 0x63 0x00 0x6B 0x00 0x65 0x00 0x74 0x00 0x20 0x00 0x62 0x00
|
||||||
|
* 0x65 0x00 0x67 0x00 0x69 0x00 0x6E 0x00 0x3D [0x00]
|
||||||
|
* </li>
|
||||||
|
* <li>32-bit encoding (UCS-4):
|
||||||
|
* As 16 bit UCS2, with three 0x00 instead of one.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
private static final byte[] XMP_PACKET_BEGIN = {
|
||||||
|
0x3C, 0x3F, 0x78, 0x70, 0x61, 0x63, 0x6B, 0x65, 0x74, 0x20,
|
||||||
|
0x62, 0x65, 0x67, 0x69, 0x6E, 0x3D
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code <?xpacket end=}
|
||||||
|
*/
|
||||||
|
private static final byte[] XMP_PACKET_END = {
|
||||||
|
0x3C, 0x3F, 0x78, 0x70, 0x61, 0x63, 0x6B, 0x65, 0x74, 0x20,
|
||||||
|
0x65, 0x6E, 0x64, 0x3D
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans the given input for an XML metadata packet.
|
||||||
|
* The scanning process involves reading every byte in the file, while searching for an XMP packet.
|
||||||
|
* This process is very inefficient, compared to reading a known file format.
|
||||||
|
* <p/>
|
||||||
|
* <em>NOTE: The XMP Specification says this method of reading an XMP packet
|
||||||
|
* should be considered a last resort.</em><br/>
|
||||||
|
* This is because files may contain multiple XMP packets, some which may be related to embedded resources,
|
||||||
|
* some which may be obsolete (or even incomplete).
|
||||||
|
*
|
||||||
|
* @param pInput the input to scan. The input may be an {@link javax.imageio.stream.ImageInputStream} or
|
||||||
|
* any object that can be passed to {@link ImageIO#createImageInputStream(Object)}.
|
||||||
|
* Typically this may be a {@link File}, {@link InputStream} or {@link java.io.RandomAccessFile}.
|
||||||
|
*
|
||||||
|
* @return a character Reader
|
||||||
|
*
|
||||||
|
* @throws java.nio.charset.UnsupportedCharsetException if the encoding specified within the BOM is not supported
|
||||||
|
* by the JRE.
|
||||||
|
* @throws IOException if an I/O exception occurs reading from {@code pInput}.
|
||||||
|
* @see ImageIO#createImageInputStream(Object)
|
||||||
|
*/
|
||||||
|
static public Reader scanForXMPPacket(final Object pInput) throws IOException {
|
||||||
|
ImageInputStream stream = pInput instanceof ImageInputStream ? (ImageInputStream) pInput : ImageIO.createImageInputStream(pInput);
|
||||||
|
|
||||||
|
// TODO: Consider if BufferedIIS is a good idea
|
||||||
|
if (!(stream instanceof BufferedImageInputStream)) {
|
||||||
|
stream = new BufferedImageInputStream(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Might be more than one XMP block per file (it's possible to re-start for now)..
|
||||||
|
long pos;
|
||||||
|
pos = scanForSequence(stream, XMP_PACKET_BEGIN);
|
||||||
|
|
||||||
|
if (pos >= 0) {
|
||||||
|
// Skip ' OR " (plus possible nulls for 16/32 bit)
|
||||||
|
byte quote = stream.readByte();
|
||||||
|
|
||||||
|
if (quote == '\'' || quote == '"') {
|
||||||
|
Charset cs = null;
|
||||||
|
|
||||||
|
// Read BOM
|
||||||
|
byte[] bom = new byte[4];
|
||||||
|
stream.readFully(bom);
|
||||||
|
|
||||||
|
// NOTE: Empty string should be treated as UTF-8 for backwards compatibility
|
||||||
|
if (bom[0] == (byte) 0xEF && bom[1] == (byte) 0xBB && bom[2] == (byte) 0xBF && bom[3] == quote ||
|
||||||
|
bom[0] == quote) {
|
||||||
|
// UTF-8
|
||||||
|
cs = Charset.forName("UTF-8");
|
||||||
|
}
|
||||||
|
else if (bom[0] == (byte) 0xFE && bom[1] == (byte) 0xFF && bom[2] == 0x00 && bom[3] == quote) {
|
||||||
|
// UTF-16 BIG endian
|
||||||
|
cs = Charset.forName("UTF-16BE");
|
||||||
|
}
|
||||||
|
else if (bom[0] == 0x00 && bom[1] == (byte) 0xFF && bom[2] == (byte) 0xFE && bom[3] == quote) {
|
||||||
|
stream.skipBytes(1); // Alignment
|
||||||
|
|
||||||
|
// UTF-16 little endian
|
||||||
|
cs = Charset.forName("UTF-16LE");
|
||||||
|
}
|
||||||
|
else if (bom[0] == 0x00 && bom[1] == 0x00 && bom[2] == (byte) 0xFE && bom[3] == (byte) 0xFF) {
|
||||||
|
// NOTE: 32-bit character set not supported by default
|
||||||
|
// UTF 32 BIG endian
|
||||||
|
cs = Charset.forName("UTF-32BE");
|
||||||
|
}
|
||||||
|
else if (bom[0] == (byte) 0xFF && bom[1] == (byte) 0xFE && bom[2] == 0x00 && bom[3] == 0x00) {
|
||||||
|
// TODO: FixMe..
|
||||||
|
// NOTE: 32-bit character set not supported by default
|
||||||
|
// UTF 32 little endian
|
||||||
|
cs = Charset.forName("UTF-32LE");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cs != null) {
|
||||||
|
// Read all bytes until <?xpacket end= up-front or filter stream
|
||||||
|
stream.mark();
|
||||||
|
long end = scanForSequence(stream, XMP_PACKET_END);
|
||||||
|
stream.reset();
|
||||||
|
|
||||||
|
long length = end - stream.getStreamPosition();
|
||||||
|
Reader reader = new InputStreamReader(IIOUtil.createStreamAdapter(stream, length), cs);
|
||||||
|
|
||||||
|
// Skip until ?>
|
||||||
|
while (reader.read() != '>') {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return reader?
|
||||||
|
// How to decide between w or r?!
|
||||||
|
return reader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans for a given ASCII sequence.
|
||||||
|
*
|
||||||
|
* @param pStream the stream to scan
|
||||||
|
* @param pSequence the byte sequence to search for
|
||||||
|
*
|
||||||
|
* @return the start position of the given sequence.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O exception occurs during scanning
|
||||||
|
*/
|
||||||
|
private static long scanForSequence(final ImageInputStream pStream, final byte[] pSequence) throws IOException {
|
||||||
|
long start = -1l;
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
int nullBytes = 0;
|
||||||
|
|
||||||
|
for (int read; (read = pStream.read()) >= 0;) {
|
||||||
|
if (pSequence[index] == (byte) read) {
|
||||||
|
// If this is the first byte in the sequence, store position
|
||||||
|
if (start == -1) {
|
||||||
|
start = pStream.getStreamPosition() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inside the sequence, there might be 1 or 3 null bytes, depending on 16/32 byte encoding
|
||||||
|
if (nullBytes == 1 || nullBytes == 3) {
|
||||||
|
pStream.skipBytes(nullBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
index++;
|
||||||
|
|
||||||
|
// If we found the entire sequence, we're done, return start position
|
||||||
|
if (index == pSequence.length) {
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (index == 1 && read == 0 && nullBytes < 3) {
|
||||||
|
// Skip 1 or 3 null bytes for 16/32 bit encoding
|
||||||
|
nullBytes++;
|
||||||
|
}
|
||||||
|
else if (index != 0) {
|
||||||
|
// Start over
|
||||||
|
index = 0;
|
||||||
|
start = -1;
|
||||||
|
nullBytes = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1l;
|
||||||
|
}
|
||||||
|
|
||||||
|
//static public XMPDirectory parse(input);
|
||||||
|
|
||||||
|
public static void main(final String[] pArgs) throws IOException {
|
||||||
|
ImageInputStream stream = ImageIO.createImageInputStream(new File(pArgs[0]));
|
||||||
|
|
||||||
|
Reader xmp;
|
||||||
|
while ((xmp = scanForXMPPacket(stream)) != null) {
|
||||||
|
BufferedReader reader = new BufferedReader(xmp);
|
||||||
|
String line;
|
||||||
|
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
System.out.println(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.close();
|
||||||
|
// else {
|
||||||
|
// System.err.println("XMP not found");
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ import java.util.Arrays;
|
|||||||
* @version $Id: AbstractMetadata.java,v 1.0 Nov 13, 2009 1:02:12 AM haraldk Exp$
|
* @version $Id: AbstractMetadata.java,v 1.0 Nov 13, 2009 1:02:12 AM haraldk Exp$
|
||||||
*/
|
*/
|
||||||
abstract class AbstractMetadata extends IIOMetadata implements Cloneable {
|
abstract class AbstractMetadata extends IIOMetadata implements Cloneable {
|
||||||
|
// TODO: Move to core...
|
||||||
|
|
||||||
protected AbstractMetadata(final boolean pStandardFormatSupported,
|
protected AbstractMetadata(final boolean pStandardFormatSupported,
|
||||||
final String pNativeFormatName, final String pNativeFormatClassName,
|
final String pNativeFormatName, final String pNativeFormatClassName,
|
||||||
|
@ -2,15 +2,13 @@ package com.twelvemonkeys.imageio.plugins.psd;
|
|||||||
|
|
||||||
import com.twelvemonkeys.imageio.metadata.Directory;
|
import com.twelvemonkeys.imageio.metadata.Directory;
|
||||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||||
|
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
|
||||||
|
import com.twelvemonkeys.imageio.metadata.iptc.IPTC;
|
||||||
import com.twelvemonkeys.lang.StringUtil;
|
import com.twelvemonkeys.lang.StringUtil;
|
||||||
import com.twelvemonkeys.util.FilterIterator;
|
import com.twelvemonkeys.util.FilterIterator;
|
||||||
import org.w3c.dom.Document;
|
|
||||||
import org.w3c.dom.Node;
|
import org.w3c.dom.Node;
|
||||||
import org.xml.sax.InputSource;
|
|
||||||
|
|
||||||
import javax.imageio.metadata.IIOMetadataNode;
|
import javax.imageio.metadata.IIOMetadataNode;
|
||||||
import javax.xml.parsers.DocumentBuilder;
|
|
||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
|
||||||
import java.awt.image.IndexColorModel;
|
import java.awt.image.IndexColorModel;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
@ -251,72 +249,36 @@ public final class PSDMetadata extends AbstractMetadata {
|
|||||||
}
|
}
|
||||||
else if (imageResource instanceof PSDIPTCData) {
|
else if (imageResource instanceof PSDIPTCData) {
|
||||||
// TODO: Revise/rethink this...
|
// TODO: Revise/rethink this...
|
||||||
// Transcode to XMP? ;-)
|
|
||||||
PSDIPTCData iptc = (PSDIPTCData) imageResource;
|
PSDIPTCData iptc = (PSDIPTCData) imageResource;
|
||||||
|
|
||||||
node = new IIOMetadataNode("Directory");
|
node = new IIOMetadataNode("DirectoryResource");
|
||||||
node.setAttribute("type", "IPTC");
|
node.setAttribute("type", "IPTC");
|
||||||
node.setUserObject(iptc.mDirectory);
|
node.setUserObject(iptc.mDirectory);
|
||||||
|
|
||||||
for (Entry entry : iptc.mDirectory) {
|
appendEntries(node, "IPTC", iptc.mDirectory);
|
||||||
IIOMetadataNode tag = new IIOMetadataNode("Entry");
|
|
||||||
tag.setAttribute("tag", String.format("%d:%02d", (Integer) entry.getIdentifier() >> 8, (Integer) entry.getIdentifier() & 0xff));
|
|
||||||
|
|
||||||
String field = entry.getFieldName();
|
|
||||||
if (field != null) {
|
|
||||||
tag.setAttribute("field", String.format("%s", field));
|
|
||||||
}
|
|
||||||
tag.setAttribute("value", entry.getValueAsString());
|
|
||||||
|
|
||||||
String type = entry.getTypeName();
|
|
||||||
if (type != null) {
|
|
||||||
tag.setAttribute("type", type);
|
|
||||||
}
|
|
||||||
node.appendChild(tag);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (imageResource instanceof PSDEXIF1Data) {
|
else if (imageResource instanceof PSDEXIF1Data) {
|
||||||
// TODO: Revise/rethink this...
|
// TODO: Revise/rethink this...
|
||||||
// Transcode to XMP? ;-)
|
|
||||||
PSDEXIF1Data exif = (PSDEXIF1Data) imageResource;
|
PSDEXIF1Data exif = (PSDEXIF1Data) imageResource;
|
||||||
|
|
||||||
node = new IIOMetadataNode("Directory");
|
node = new IIOMetadataNode("DirectoryResource");
|
||||||
node.setAttribute("type", "EXIF");
|
node.setAttribute("type", "EXIF");
|
||||||
// TODO: Set byte[] data instead
|
// TODO: Set byte[] data instead
|
||||||
node.setUserObject(exif.mDirectory);
|
node.setUserObject(exif.mDirectory);
|
||||||
|
|
||||||
appendEntries(node, exif.mDirectory);
|
appendEntries(node, "EXIF", exif.mDirectory);
|
||||||
}
|
}
|
||||||
else if (imageResource instanceof PSDXMPData) {
|
else if (imageResource instanceof PSDXMPData) {
|
||||||
// TODO: Revise/rethink this... Would it be possible to parse XMP as IIOMetadataNodes? Or is that just stupid...
|
// TODO: Revise/rethink this... Would it be possible to parse XMP as IIOMetadataNodes? Or is that just stupid...
|
||||||
// Or maybe use the Directory approach used by IPTC and EXIF..
|
// Or maybe use the Directory approach used by IPTC and EXIF..
|
||||||
PSDXMPData xmp = (PSDXMPData) imageResource;
|
PSDXMPData xmp = (PSDXMPData) imageResource;
|
||||||
|
|
||||||
node = new IIOMetadataNode("XMP");
|
node = new IIOMetadataNode("DirectoryResource");
|
||||||
|
node.setAttribute("type", "XMP");
|
||||||
|
appendEntries(node, "XMP", xmp.mDirectory);
|
||||||
|
|
||||||
try {
|
// Set the entire XMP document as user data
|
||||||
// BufferedReader reader = new BufferedReader(xmp.getData());
|
node.setUserObject(xmp.mData);
|
||||||
// String line;
|
|
||||||
// while ((line = reader.readLine()) != null) {
|
|
||||||
// System.out.println(line);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
DocumentBuilder builder;
|
|
||||||
Document document;
|
|
||||||
|
|
||||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
|
||||||
factory.setNamespaceAware(true);
|
|
||||||
builder = factory.newDocumentBuilder();
|
|
||||||
document = builder.parse(new InputSource(xmp.getData()));
|
|
||||||
|
|
||||||
|
|
||||||
// Set the entire XMP document as user data
|
|
||||||
node.setUserObject(document);
|
|
||||||
// node.appendChild(document.getFirstChild());
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Generic resource..
|
// Generic resource..
|
||||||
@ -342,18 +304,25 @@ public final class PSDMetadata extends AbstractMetadata {
|
|||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void appendEntries(IIOMetadataNode pNode, final Directory pDirectory) {
|
private void appendEntries(final IIOMetadataNode pNode, final String pType, final Directory pDirectory) {
|
||||||
for (Entry entry : pDirectory) {
|
for (Entry entry : pDirectory) {
|
||||||
|
Object tagId = entry.getIdentifier();
|
||||||
|
|
||||||
IIOMetadataNode tag = new IIOMetadataNode("Entry");
|
IIOMetadataNode tag = new IIOMetadataNode("Entry");
|
||||||
tag.setAttribute("tag", String.format("%s", entry.getIdentifier()));
|
tag.setAttribute("tag", String.format("%s", tagId));
|
||||||
|
|
||||||
String field = entry.getFieldName();
|
String field = entry.getFieldName();
|
||||||
if (field != null) {
|
if (field != null) {
|
||||||
tag.setAttribute("field", String.format("%s", field));
|
tag.setAttribute("field", String.format("%s", field));
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
if ("IPTC".equals(pType)) {
|
||||||
|
tag.setAttribute("field", String.format("%s:%s", (Integer) tagId >> 8, (Integer) tagId & 0xff));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (entry.getValue() instanceof Directory) {
|
if (entry.getValue() instanceof Directory) {
|
||||||
appendEntries(tag, (Directory) entry.getValue());
|
appendEntries(tag, pType, (Directory) entry.getValue());
|
||||||
tag.setAttribute("type", "Directory");
|
tag.setAttribute("type", "Directory");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -614,7 +583,7 @@ public final class PSDMetadata extends AbstractMetadata {
|
|||||||
PSDEXIF1Data data = exif.next();
|
PSDEXIF1Data data = exif.next();
|
||||||
|
|
||||||
// Get the EXIF DateTime (aka ModifyDate) tag if present
|
// Get the EXIF DateTime (aka ModifyDate) tag if present
|
||||||
Entry dateTime = data.mDirectory.getEntryById(0x0132); // TODO: Constant
|
Entry dateTime = data.mDirectory.getEntryById(TIFF.TAG_DATE_TIME);
|
||||||
if (dateTime != null) {
|
if (dateTime != null) {
|
||||||
node = new IIOMetadataNode("ImageCreationTime"); // As TIFF, but could just as well be ImageModificationTime
|
node = new IIOMetadataNode("ImageCreationTime"); // As TIFF, but could just as well be ImageModificationTime
|
||||||
// Format: "YYYY:MM:DD hh:mm:ss"
|
// Format: "YYYY:MM:DD hh:mm:ss"
|
||||||
@ -642,7 +611,7 @@ public final class PSDMetadata extends AbstractMetadata {
|
|||||||
// Example: TIFF Software field => /Text/TextEntry@keyword = "Software",
|
// Example: TIFF Software field => /Text/TextEntry@keyword = "Software",
|
||||||
// /Text/TextEntry@value = Name and version number of the software package(s) used to create the image.
|
// /Text/TextEntry@value = Name and version number of the software package(s) used to create the image.
|
||||||
|
|
||||||
Iterator<PSDImageResource> textResources = getResources(PSDEXIF1Data.class, PSDIPTCData.class, PSDXMPData.class);
|
Iterator<PSDImageResource> textResources = getResources(PSD.RES_IPTC_NAA, PSD.RES_EXIF_DATA_1, PSD.RES_XMP_DATA);
|
||||||
|
|
||||||
if (!textResources.hasNext()) {
|
if (!textResources.hasNext()) {
|
||||||
return null;
|
return null;
|
||||||
@ -660,24 +629,40 @@ public final class PSDMetadata extends AbstractMetadata {
|
|||||||
if (textResource instanceof PSDIPTCData) {
|
if (textResource instanceof PSDIPTCData) {
|
||||||
PSDIPTCData iptc = (PSDIPTCData) textResource;
|
PSDIPTCData iptc = (PSDIPTCData) textResource;
|
||||||
|
|
||||||
for (Entry entry : iptc.mDirectory) {
|
appendTextEntriesFlat(text, iptc.mDirectory, new FilterIterator.Filter<Entry>() {
|
||||||
node = new IIOMetadataNode("TextEntry");
|
public boolean accept(final Entry pEntry) {
|
||||||
|
Integer tagId = (Integer) pEntry.getIdentifier();
|
||||||
|
|
||||||
if (entry.getValue() instanceof String) {
|
switch (tagId) {
|
||||||
node.setAttribute("keyword", String.format("%s", entry.getFieldName()));
|
case IPTC.TAG_SOURCE:
|
||||||
node.setAttribute("value", entry.getValueAsString());
|
return true;
|
||||||
text.appendChild(node);
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
else if (textResource instanceof PSDEXIF1Data) {
|
else if (textResource instanceof PSDEXIF1Data) {
|
||||||
PSDEXIF1Data exif = (PSDEXIF1Data) textResource;
|
PSDEXIF1Data exif = (PSDEXIF1Data) textResource;
|
||||||
|
|
||||||
// TODO: Use name?
|
appendTextEntriesFlat(text, exif.mDirectory, new FilterIterator.Filter<Entry>() {
|
||||||
appendTextEntriesFlat(text, exif.mDirectory);
|
public boolean accept(final Entry pEntry) {
|
||||||
|
Integer tagId = (Integer) pEntry.getIdentifier();
|
||||||
|
|
||||||
|
switch (tagId) {
|
||||||
|
case TIFF.TAG_SOFTWARE:
|
||||||
|
case TIFF.TAG_ARTIST:
|
||||||
|
case TIFF.TAG_COPYRIGHT:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else if (textResource instanceof PSDXMPData) {
|
else if (textResource instanceof PSDXMPData) {
|
||||||
// TODO: Parse XMP (heavy) ONLY if we don't have required fields from IPTC/EXIF?
|
// TODO: Parse XMP (heavy) ONLY if we don't have required fields from IPTC/EXIF?
|
||||||
|
// TODO: Use XMP IPTC/EXIF/TIFFF NativeDigest field to validate if the values are in sync...
|
||||||
PSDXMPData xmp = (PSDXMPData) textResource;
|
PSDXMPData xmp = (PSDXMPData) textResource;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -685,15 +670,26 @@ public final class PSDMetadata extends AbstractMetadata {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void appendTextEntriesFlat(IIOMetadataNode pNode, Directory pDirectory) {
|
private void appendTextEntriesFlat(final IIOMetadataNode pNode, final Directory pDirectory, final FilterIterator.Filter<Entry> pFilter) {
|
||||||
for (Entry entry : pDirectory) {
|
FilterIterator<Entry> pEntries = new FilterIterator<Entry>(pDirectory.iterator(), pFilter);
|
||||||
|
while (pEntries.hasNext()) {
|
||||||
|
Entry entry = pEntries.next();
|
||||||
|
|
||||||
if (entry.getValue() instanceof Directory) {
|
if (entry.getValue() instanceof Directory) {
|
||||||
appendTextEntriesFlat(pNode, (Directory) entry.getValue());
|
appendTextEntriesFlat(pNode, (Directory) entry.getValue(), pFilter);
|
||||||
}
|
}
|
||||||
else if (entry.getValue() instanceof String) {
|
else if (entry.getValue() instanceof String) {
|
||||||
IIOMetadataNode tag = new IIOMetadataNode("TextEntry");
|
IIOMetadataNode tag = new IIOMetadataNode("TextEntry");
|
||||||
// TODO: Use name!
|
String fieldName = entry.getFieldName();
|
||||||
tag.setAttribute("keyword", String.format("%s", entry.getFieldName()));
|
|
||||||
|
if (fieldName != null) {
|
||||||
|
tag.setAttribute("keyword", String.format("%s", fieldName));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// TODO: This should never happen, as we filter out only specific nodes
|
||||||
|
tag.setAttribute("keyword", String.format("%s", entry.getIdentifier()));
|
||||||
|
}
|
||||||
|
|
||||||
tag.setAttribute("value", entry.getValueAsString());
|
tag.setAttribute("value", entry.getValueAsString());
|
||||||
pNode.appendChild(tag);
|
pNode.appendChild(tag);
|
||||||
}
|
}
|
||||||
@ -734,13 +730,13 @@ public final class PSDMetadata extends AbstractMetadata {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Iterator<PSDImageResource> getResources(final Class<? extends PSDImageResource>... pResourceTypes) {
|
Iterator<PSDImageResource> getResources(final int... pResourceTypes) {
|
||||||
Iterator<PSDImageResource> iterator = mImageResources.iterator();
|
Iterator<PSDImageResource> iterator = mImageResources.iterator();
|
||||||
|
|
||||||
return new FilterIterator<PSDImageResource>(iterator, new FilterIterator.Filter<PSDImageResource>() {
|
return new FilterIterator<PSDImageResource>(iterator, new FilterIterator.Filter<PSDImageResource>() {
|
||||||
public boolean accept(final PSDImageResource pElement) {
|
public boolean accept(final PSDImageResource pResource) {
|
||||||
for (Class<?> type : pResourceTypes) {
|
for (int type : pResourceTypes) {
|
||||||
if (type.isInstance(pElement)) {
|
if (type == pResource.mId) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package com.twelvemonkeys.imageio.plugins.psd;
|
package com.twelvemonkeys.imageio.plugins.psd;
|
||||||
|
|
||||||
|
import com.twelvemonkeys.imageio.metadata.Directory;
|
||||||
|
import com.twelvemonkeys.imageio.metadata.xmp.XMPReader;
|
||||||
import com.twelvemonkeys.lang.StringUtil;
|
import com.twelvemonkeys.lang.StringUtil;
|
||||||
|
|
||||||
import javax.imageio.stream.ImageInputStream;
|
import javax.imageio.stream.ImageInputStream;
|
||||||
@ -21,6 +23,7 @@ import java.nio.charset.Charset;
|
|||||||
*/
|
*/
|
||||||
final class PSDXMPData extends PSDImageResource {
|
final class PSDXMPData extends PSDImageResource {
|
||||||
protected byte[] mData;
|
protected byte[] mData;
|
||||||
|
Directory mDirectory;
|
||||||
|
|
||||||
PSDXMPData(final short pId, final ImageInputStream pInput) throws IOException {
|
PSDXMPData(final short pId, final ImageInputStream pInput) throws IOException {
|
||||||
super(pId, pInput);
|
super(pId, pInput);
|
||||||
@ -29,7 +32,9 @@ final class PSDXMPData extends PSDImageResource {
|
|||||||
@Override
|
@Override
|
||||||
protected void readData(final ImageInputStream pInput) throws IOException {
|
protected void readData(final ImageInputStream pInput) throws IOException {
|
||||||
mData = new byte[(int) mSize]; // TODO: Fix potential overflow, or document why that can't happen (read spec)
|
mData = new byte[(int) mSize]; // TODO: Fix potential overflow, or document why that can't happen (read spec)
|
||||||
pInput.readFully(mData);
|
//pInput.readFully(mData);
|
||||||
|
|
||||||
|
mDirectory = new XMPReader().read(pInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
Loading…
x
Reference in New Issue
Block a user