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:
Harald Kuhr 2009-11-18 00:43:18 +01:00
parent d24c2c1b08
commit 64b21b83bb
19 changed files with 709 additions and 158 deletions

View File

@ -13,7 +13,6 @@ import java.util.List;
* @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) {
@ -32,9 +31,9 @@ public abstract class AbstractDirectory implements Directory {
return null;
}
public Entry getEntryByName(final String pName) {
public Entry getEntryByFieldName(final String pFieldName) {
for (Entry entry : this) {
if (entry.getFieldName().equals(pName)) {
if (entry.getFieldName() != null && entry.getFieldName().equals(pFieldName)) {
return entry;
}
}
@ -66,6 +65,7 @@ public abstract class AbstractDirectory implements Directory {
return mEntries.add(pEntry);
}
@SuppressWarnings({"SuspiciousMethodCalls"})
public boolean remove(final Object pEntry) {
assertMutable();

View File

@ -23,7 +23,7 @@ public abstract class AbstractEntry implements Entry {
mValue = pValue;
}
public Object getIdentifier() {
public final Object getIdentifier() {
return mIdentifier;
}

View File

@ -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
Entry getEntryById(Object pIdentifier);
Entry getEntryByName(String pName);
Entry getEntryByFieldName(String pName);
// Iterator containing the entries in
//Iterator<Entry> getBestEntries(Object pIdentifier, Object pQualifier, String pLanguage);

View File

@ -7,41 +7,8 @@ package com.twelvemonkeys.imageio.metadata.exif;
* @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;
public interface EXIF {
int TAG_COLOR_SPACE = 40961;
int TAG_PIXEL_X_DIMENSION = 40962;
int TAG_PIXEL_Y_DIMENSION = 40963;
}

View File

@ -4,7 +4,6 @@ import com.twelvemonkeys.imageio.metadata.AbstractDirectory;
import com.twelvemonkeys.imageio.metadata.Entry;
import java.util.Collection;
import java.util.List;
/**
* EXIFDirectory

View File

@ -12,19 +12,42 @@ import com.twelvemonkeys.imageio.metadata.AbstractEntry;
final class EXIFEntry extends AbstractEntry {
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);
if (pType < 1 || pType > TIFF.TYPE_NAMES.length) {
throw new IllegalArgumentException(String.format("Illegal EXIF type: %s", pType));
}
mType = pType;
}
@Override
public String getFieldName() {
// TODO: Need tons of constants... ;-)
return super.getFieldName();
switch ((Integer) getIdentifier()) {
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
public String getTypeName() {
return EXIF.TYPE_NAMES[mType];
return TIFF.TYPE_NAMES[mType - 1];
}
}

View File

@ -29,12 +29,12 @@ public final class EXIFReader extends MetadataReader {
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")));
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();
if (magic != EXIF.TIFF_MAGIC) {
throw new IIOException(String.format("Wrong TIFF magic in EXIF data: %04x, expected: %04x", magic, EXIF.TIFF_MAGIC));
if (magic != TIFF.TIFF_MAGIC) {
throw new IIOException(String.format("Wrong TIFF magic in EXIF data: %04x, expected: %04x", magic, TIFF.TIFF_MAGIC));
}
long directoryOffset = pInput.readUnsignedInt();
@ -73,9 +73,8 @@ public final class EXIFReader extends MetadataReader {
Object value;
// TODO: Handle other sub-IFDs
// GPS IFD: 0x8825, Interoperability IFD: 0xA005
if (tagId == EXIF.EXIF_IFD) {
if (tagId == TIFF.IFD_EXIF || tagId == TIFF.IFD_GPS || tagId == TIFF.IFD_INTEROP) {
// Parse sub IFDs
long offset = pInput.readUnsignedInt();
pInput.mark();
@ -207,8 +206,8 @@ public final class EXIFReader extends MetadataReader {
}
private int getValueLength(final int pType, final int pCount) {
if (pType > 0 && pType <= EXIF.TYPE_LENGTHS.length) {
return EXIF.TYPE_LENGTHS[pType - 1] * pCount;
if (pType > 0 && pType <= TIFF.TYPE_LENGTHS.length) {
return TIFF.TYPE_LENGTHS[pType - 1] * pCount;
}
return -1;

View File

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

View File

@ -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$
*/
class IPTCEntry extends AbstractEntry {
public IPTCEntry(int pTagId, Object pValue) {
public IPTCEntry(final int pTagId, final Object pValue) {
super(pTagId, pValue);
}
@Override
public String getFieldName() {
switch ((Integer) getIdentifier()) {
case IPTC.TAG_SOURCE:
return "Source";
// TODO: More tags...
}
return null;
}
}

View File

@ -24,7 +24,7 @@ import java.util.List;
* @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 {
public final 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;
@ -38,10 +38,10 @@ public class IPTCReader extends MetadataReader {
// 0x1c identifies start of a tag
while (pInput.read() == 0x1c) {
int tagId = pInput.readShort();
short tagId = pInput.readShort();
int tagByteCount = pInput.readUnsignedShort();
Entry entry = readEntry(pInput, tagId, tagByteCount);
if (entry != null) {
entries.add(entry);
}
@ -50,7 +50,7 @@ public class IPTCReader extends MetadataReader {
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;
switch (pTagId) {
@ -63,30 +63,6 @@ public class IPTCReader extends MetadataReader {
// 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) {
@ -100,7 +76,7 @@ public class IPTCReader extends MetadataReader {
// If we don't have a value, treat it as a string
if (value == null) {
if (pLength < 1) {
value = "(No value)";
value = null;
}
else {
value = parseString(pInput, pLength);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 &lt;?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 &lt;?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");
// }
}
}

View File

@ -15,6 +15,7 @@ import java.util.Arrays;
* @version $Id: AbstractMetadata.java,v 1.0 Nov 13, 2009 1:02:12 AM haraldk Exp$
*/
abstract class AbstractMetadata extends IIOMetadata implements Cloneable {
// TODO: Move to core...
protected AbstractMetadata(final boolean pStandardFormatSupported,
final String pNativeFormatName, final String pNativeFormatClassName,

View File

@ -2,15 +2,13 @@ package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.metadata.Directory;
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.util.FilterIterator;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import javax.imageio.metadata.IIOMetadataNode;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.awt.image.IndexColorModel;
import java.util.Arrays;
import java.util.Iterator;
@ -251,72 +249,36 @@ public final class PSDMetadata extends AbstractMetadata {
}
else if (imageResource instanceof PSDIPTCData) {
// TODO: Revise/rethink this...
// Transcode to XMP? ;-)
PSDIPTCData iptc = (PSDIPTCData) imageResource;
node = new IIOMetadataNode("Directory");
node = new IIOMetadataNode("DirectoryResource");
node.setAttribute("type", "IPTC");
node.setUserObject(iptc.mDirectory);
for (Entry entry : 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);
}
appendEntries(node, "IPTC", iptc.mDirectory);
}
else if (imageResource instanceof PSDEXIF1Data) {
// TODO: Revise/rethink this...
// Transcode to XMP? ;-)
PSDEXIF1Data exif = (PSDEXIF1Data) imageResource;
node = new IIOMetadataNode("Directory");
node = new IIOMetadataNode("DirectoryResource");
node.setAttribute("type", "EXIF");
// TODO: Set byte[] data instead
node.setUserObject(exif.mDirectory);
appendEntries(node, exif.mDirectory);
appendEntries(node, "EXIF", exif.mDirectory);
}
else if (imageResource instanceof PSDXMPData) {
// 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..
PSDXMPData xmp = (PSDXMPData) imageResource;
node = new IIOMetadataNode("XMP");
try {
// BufferedReader reader = new BufferedReader(xmp.getData());
// 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()));
node = new IIOMetadataNode("DirectoryResource");
node.setAttribute("type", "XMP");
appendEntries(node, "XMP", xmp.mDirectory);
// Set the entire XMP document as user data
node.setUserObject(document);
// node.appendChild(document.getFirstChild());
}
catch (Exception e) {
e.printStackTrace();
}
node.setUserObject(xmp.mData);
}
else {
// Generic resource..
@ -342,18 +304,25 @@ public final class PSDMetadata extends AbstractMetadata {
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) {
Object tagId = entry.getIdentifier();
IIOMetadataNode tag = new IIOMetadataNode("Entry");
tag.setAttribute("tag", String.format("%s", entry.getIdentifier()));
tag.setAttribute("tag", String.format("%s", tagId));
String field = entry.getFieldName();
if (field != null) {
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) {
appendEntries(tag, (Directory) entry.getValue());
appendEntries(tag, pType, (Directory) entry.getValue());
tag.setAttribute("type", "Directory");
}
else {
@ -614,7 +583,7 @@ public final class PSDMetadata extends AbstractMetadata {
PSDEXIF1Data data = exif.next();
// 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) {
node = new IIOMetadataNode("ImageCreationTime"); // As TIFF, but could just as well be ImageModificationTime
// 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",
// /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()) {
return null;
@ -660,24 +629,40 @@ public final class PSDMetadata extends AbstractMetadata {
if (textResource instanceof PSDIPTCData) {
PSDIPTCData iptc = (PSDIPTCData) textResource;
for (Entry entry : iptc.mDirectory) {
node = new IIOMetadataNode("TextEntry");
appendTextEntriesFlat(text, iptc.mDirectory, new FilterIterator.Filter<Entry>() {
public boolean accept(final Entry pEntry) {
Integer tagId = (Integer) pEntry.getIdentifier();
if (entry.getValue() instanceof String) {
node.setAttribute("keyword", String.format("%s", entry.getFieldName()));
node.setAttribute("value", entry.getValueAsString());
text.appendChild(node);
switch (tagId) {
case IPTC.TAG_SOURCE:
return true;
default:
return false;
}
}
});
}
else if (textResource instanceof PSDEXIF1Data) {
PSDEXIF1Data exif = (PSDEXIF1Data) textResource;
// TODO: Use name?
appendTextEntriesFlat(text, exif.mDirectory);
appendTextEntriesFlat(text, exif.mDirectory, new FilterIterator.Filter<Entry>() {
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) {
// 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;
}
}
@ -685,15 +670,26 @@ public final class PSDMetadata extends AbstractMetadata {
return text;
}
private void appendTextEntriesFlat(IIOMetadataNode pNode, Directory pDirectory) {
for (Entry entry : pDirectory) {
private void appendTextEntriesFlat(final IIOMetadataNode pNode, final Directory pDirectory, final FilterIterator.Filter<Entry> pFilter) {
FilterIterator<Entry> pEntries = new FilterIterator<Entry>(pDirectory.iterator(), pFilter);
while (pEntries.hasNext()) {
Entry entry = pEntries.next();
if (entry.getValue() instanceof Directory) {
appendTextEntriesFlat(pNode, (Directory) entry.getValue());
appendTextEntriesFlat(pNode, (Directory) entry.getValue(), pFilter);
}
else if (entry.getValue() instanceof String) {
IIOMetadataNode tag = new IIOMetadataNode("TextEntry");
// TODO: Use name!
tag.setAttribute("keyword", String.format("%s", entry.getFieldName()));
String fieldName = 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());
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();
return new FilterIterator<PSDImageResource>(iterator, new FilterIterator.Filter<PSDImageResource>() {
public boolean accept(final PSDImageResource pElement) {
for (Class<?> type : pResourceTypes) {
if (type.isInstance(pElement)) {
public boolean accept(final PSDImageResource pResource) {
for (int type : pResourceTypes) {
if (type == pResource.mId) {
return true;
}
}

View File

@ -1,5 +1,7 @@
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 javax.imageio.stream.ImageInputStream;
@ -21,6 +23,7 @@ import java.nio.charset.Charset;
*/
final class PSDXMPData extends PSDImageResource {
protected byte[] mData;
Directory mDirectory;
PSDXMPData(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
@ -29,7 +32,9 @@ final class PSDXMPData extends PSDImageResource {
@Override
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)
pInput.readFully(mData);
//pInput.readFully(mData);
mDirectory = new XMPReader().read(pInput);
}
@Override