diff --git a/twelvemonkeys-imageio/metadata/pom.xml b/twelvemonkeys-imageio/metadata/pom.xml new file mode 100644 index 00000000..5ab79418 --- /dev/null +++ b/twelvemonkeys-imageio/metadata/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + com.twelvemonkeys.imageio + twelvemonkeys-imageio-metadata + 2.3-SNAPSHOT + TwelveMonkeys ImageIO Metadata + + ImageIO metadata module. + + + + twelvemonkeys-imageio + com.twelvemonkeys + 2.3-SNAPSHOT + + + + + com.twelvemonkeys.imageio + twelvemonkeys-imageio-core + + + com.twelvemonkeys.imageio + twelvemonkeys-imageio-core + tests + + + diff --git a/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractDirectory.java b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractDirectory.java new file mode 100644 index 00000000..dbad3f8c --- /dev/null +++ b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractDirectory.java @@ -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 Harald Kuhr + * @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 mEntries = new ArrayList(); + + protected AbstractDirectory(final Collection 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 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()); + } +} diff --git a/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java new file mode 100644 index 00000000..e03a917d --- /dev/null +++ b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/AbstractEntry.java @@ -0,0 +1,99 @@ +package com.twelvemonkeys.imageio.metadata; + +import com.twelvemonkeys.lang.Validate; + +import java.lang.reflect.Array; + +/** + * AbstractEntry + * + * @author Harald Kuhr + * @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); + } +} diff --git a/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Directory.java b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Directory.java new file mode 100644 index 00000000..66fa5011 --- /dev/null +++ b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Directory.java @@ -0,0 +1,33 @@ +package com.twelvemonkeys.imageio.metadata; + +/** + * Directory + * + * @author Harald Kuhr + * @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 { + // 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 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(); +} diff --git a/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Entry.java b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Entry.java new file mode 100644 index 00000000..3b8f7da2 --- /dev/null +++ b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/Entry.java @@ -0,0 +1,39 @@ +package com.twelvemonkeys.imageio.metadata; + +/** + * Entry + * + * @author Harald Kuhr + * @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(); +} diff --git a/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/MetadataReader.java b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/MetadataReader.java new file mode 100644 index 00000000..d67aff25 --- /dev/null +++ b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/MetadataReader.java @@ -0,0 +1,15 @@ +package com.twelvemonkeys.imageio.metadata; + +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; + +/** + * MetadataReader + * + * @author Harald Kuhr + * @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; +} diff --git a/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIF.java b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIF.java new file mode 100644 index 00000000..4fc8c22f --- /dev/null +++ b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIF.java @@ -0,0 +1,47 @@ +package com.twelvemonkeys.imageio.metadata.exif; + +/** + * EXIF + * + * @author Harald Kuhr + * @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; +} diff --git a/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFDirectory.java b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFDirectory.java new file mode 100644 index 00000000..98d426de --- /dev/null +++ b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFDirectory.java @@ -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 Harald Kuhr + * @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 pEntries) { + super(pEntries); + } +} diff --git a/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java new file mode 100644 index 00000000..7ed7cbb5 --- /dev/null +++ b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java @@ -0,0 +1,30 @@ +package com.twelvemonkeys.imageio.metadata.exif; + +import com.twelvemonkeys.imageio.metadata.AbstractEntry; + +/** + * EXIFEntry + * + * @author Harald Kuhr + * @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]; + } +} diff --git a/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java new file mode 100644 index 00000000..d5a406ca --- /dev/null +++ b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFReader.java @@ -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 Harald Kuhr + * @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 entries = new ArrayList(); + + 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; + } +} diff --git a/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTC.java b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTC.java new file mode 100644 index 00000000..a762f6bb --- /dev/null +++ b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTC.java @@ -0,0 +1,130 @@ +package com.twelvemonkeys.imageio.metadata.iptc; + +/** + * IPTC + * + * @author Harald Kuhr + * @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 JobMinder Homepage + */ + 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 +} diff --git a/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCDirectory.java b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCDirectory.java new file mode 100644 index 00000000..e3bb2745 --- /dev/null +++ b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCDirectory.java @@ -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 Harald Kuhr + * @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 pEntries) { + super(pEntries); + } +} \ No newline at end of file diff --git a/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCEntry.java b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCEntry.java new file mode 100644 index 00000000..8773fc48 --- /dev/null +++ b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCEntry.java @@ -0,0 +1,16 @@ +package com.twelvemonkeys.imageio.metadata.iptc; + +import com.twelvemonkeys.imageio.metadata.AbstractEntry; + +/** +* IPTCEntry +* +* @author Harald Kuhr +* @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); + } +} diff --git a/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCReader.java b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCReader.java new file mode 100644 index 00000000..863f039b --- /dev/null +++ b/twelvemonkeys-imageio/metadata/src/main/java/com/twelvemonkeys/imageio/metadata/iptc/IPTCReader.java @@ -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 Harald Kuhr + * @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 entries = new ArrayList(); + + // 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"); + } + } + +} diff --git a/twelvemonkeys-imageio/metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/XMPScannerTestCase.java b/twelvemonkeys-imageio/metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/XMPScannerTestCase.java new file mode 100644 index 00000000..9d4e9a96 --- /dev/null +++ b/twelvemonkeys-imageio/metadata/src/test/java/com/twelvemonkeys/imageio/metadata/xmp/XMPScannerTestCase.java @@ -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 Harald Kuhr + * @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 = + "" + + "\n"+ + " \n"+ + " \n"+ + " twelvemonkeys.com\n"+ + " \n"+ + " \n"+ + " application/vnd.adobe.photoshop\n"+ + " \n"+ + " \n"+ + "" + + ""; + + 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); +// } +} diff --git a/twelvemonkeys-imageio/pom.xml b/twelvemonkeys-imageio/pom.xml index cd33e3fc..97bd4e9e 100644 --- a/twelvemonkeys-imageio/pom.xml +++ b/twelvemonkeys-imageio/pom.xml @@ -29,6 +29,7 @@ core + metadata ico @@ -96,6 +97,13 @@ tests test + + + com.twelvemonkeys.imageio + twelvemonkeys-imageio-metadata + ${imageio.core.version} + compile + diff --git a/twelvemonkeys-imageio/psd/pom.xml b/twelvemonkeys-imageio/psd/pom.xml index 455d245c..a19dce2e 100644 --- a/twelvemonkeys-imageio/psd/pom.xml +++ b/twelvemonkeys-imageio/psd/pom.xml @@ -1,31 +1,35 @@ - - - 4.0.0 - com.twelvemonkeys.imageio - twelvemonkeys-imageio-psd - 2.3-SNAPSHOT - TwelveMonkeys ImageIO PSD plugin - - ImageIO plugin for Adobe Photoshop Document (PSD). - - - - twelvemonkeys-imageio - com.twelvemonkeys - 2.3-SNAPSHOT - - - - - com.twelvemonkeys.imageio - twelvemonkeys-imageio-core - - - com.twelvemonkeys.imageio - twelvemonkeys-imageio-core - tests - - - \ No newline at end of file + + + 4.0.0 + com.twelvemonkeys.imageio + twelvemonkeys-imageio-psd + 2.3-SNAPSHOT + TwelveMonkeys ImageIO PSD plugin + + ImageIO plugin for Adobe Photoshop Document (PSD). + + + + twelvemonkeys-imageio + com.twelvemonkeys + 2.3-SNAPSHOT + + + + + com.twelvemonkeys.imageio + twelvemonkeys-imageio-core + + + com.twelvemonkeys.imageio + twelvemonkeys-imageio-core + tests + + + com.twelvemonkeys.imageio + twelvemonkeys-imageio-metadata + + + diff --git a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/AbstractMetadata.java b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/AbstractMetadata.java new file mode 100644 index 00000000..5ba2d84a --- /dev/null +++ b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/AbstractMetadata.java @@ -0,0 +1,103 @@ +package com.twelvemonkeys.imageio.plugins.psd; + +import org.w3c.dom.Node; + +import javax.imageio.metadata.IIOInvalidTreeException; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import java.util.Arrays; + +/** + * AbstractMetadata + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: AbstractMetadata.java,v 1.0 Nov 13, 2009 1:02:12 AM haraldk Exp$ + */ +abstract class AbstractMetadata extends IIOMetadata implements Cloneable { + + protected AbstractMetadata(final boolean pStandardFormatSupported, + final String pNativeFormatName, final String pNativeFormatClassName, + final String[] pExtraFormatNames, final String[] pExtraFormatClassNames) { + super(pStandardFormatSupported, pNativeFormatName, pNativeFormatClassName, pExtraFormatNames, pExtraFormatClassNames); + } + + /** + * Default implementation returns {@code true}. + * Mutable subclasses should override this method. + * + * @return {@code true}. + */ + @Override + public boolean isReadOnly() { + return true; + } + + @Override + public Node getAsTree(final String pFormatName) { + validateFormatName(pFormatName); + + if (pFormatName.equals(nativeMetadataFormatName)) { + return getNativeTree(); + } + else if (pFormatName.equals(IIOMetadataFormatImpl.standardMetadataFormatName)) { + return getStandardTree(); + } + + // TODO: What about extra formats?? + throw new AssertionError("Unreachable"); + } + + @Override + public void mergeTree(final String pFormatName, final Node pRoot) throws IIOInvalidTreeException { + assertMutable(); + + validateFormatName(pFormatName); + + if (!pRoot.getNodeName().equals(nativeMetadataFormatName)) { + throw new IIOInvalidTreeException("Root must be " + nativeMetadataFormatName, pRoot); + } + + Node node = pRoot.getFirstChild(); + while (node != null) { + // TODO: Merge values from node into this + + // Move to the next sibling + node = node.getNextSibling(); + } + } + + @Override + public void reset() { + assertMutable(); + } + + /** + * Asserts that this meta data is mutable. + * + * @throws IllegalStateException if {@link #isReadOnly()} returns {@code true}. + */ + protected final void assertMutable() { + if (isReadOnly()) { + throw new IllegalStateException("Metadata is read-only"); + } + } + + protected abstract Node getNativeTree(); + + protected final void validateFormatName(final String pFormatName) { + String[] metadataFormatNames = getMetadataFormatNames(); + + if (metadataFormatNames != null) { + for (String metadataFormatName : metadataFormatNames) { + if (metadataFormatName.equals(pFormatName)) { + return; // Found, we're ok! + } + } + } + + throw new IllegalArgumentException( + String.format("Bad format name: \"%s\". Expected one of %s", pFormatName, Arrays.toString(metadataFormatNames)) + ); + } +} diff --git a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDAlphaChannelInfo.java b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDAlphaChannelInfo.java index ed0b4edd..9918f429 100755 --- a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDAlphaChannelInfo.java +++ b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDAlphaChannelInfo.java @@ -34,11 +34,11 @@ import java.util.ArrayList; import java.util.List; /** - * PSDAlhpaChannelInfo + * PSDAlphaChannelInfo * * @author Harald Kuhr * @author last modified by $Author: haraldk$ - * @version $Id: PSDAlhpaChannelInfo.java,v 1.0 May 2, 2008 5:33:40 PM haraldk Exp$ + * @version $Id: PSDAlphaChannelInfo.java,v 1.0 May 2, 2008 5:33:40 PM haraldk Exp$ */ class PSDAlphaChannelInfo extends PSDImageResource { List mNames; @@ -50,6 +50,7 @@ class PSDAlphaChannelInfo extends PSDImageResource { @Override protected void readData(final ImageInputStream pInput) throws IOException { mNames = new ArrayList(); + long left = mSize; while (left > 0) { String name = PSDUtil.readPascalString(pInput); diff --git a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java index 17f181aa..47a30e94 100644 --- a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java +++ b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java @@ -1,13 +1,11 @@ package com.twelvemonkeys.imageio.plugins.psd; -import com.twelvemonkeys.imageio.util.IIOUtil; +import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; import com.twelvemonkeys.lang.StringUtil; import javax.imageio.IIOException; import javax.imageio.stream.ImageInputStream; -import javax.imageio.stream.MemoryCacheImageInputStream; import java.io.IOException; -import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -26,7 +24,8 @@ import java.util.List; */ final class PSDEXIF1Data extends PSDImageResource { // protected byte[] mData; - protected Directory mDirectory; +// protected Directory mDirectory; + protected com.twelvemonkeys.imageio.metadata.Directory mDirectory; PSDEXIF1Data(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -36,24 +35,25 @@ final class PSDEXIF1Data extends PSDImageResource { protected void readData(final ImageInputStream pInput) throws IOException { // This is in essence an embedded TIFF file. // TODO: Extract TIFF parsing to more general purpose package - // TODO: Instead, read the byte data, store for later parsing (or store offset, and read on request) - MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(IIOUtil.createStreamAdapter(pInput, mSize)); - - byte[] bom = new byte[2]; - stream.readFully(bom); - if (bom[0] == 'I' && bom[1] == 'I') { - stream.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"))); - } - - if (stream.readUnsignedShort() != 42) { - throw new IIOException("Wrong TIFF magic in EXIF data."); - } - - long directoryOffset = stream.readUnsignedInt(); - mDirectory = Directory.read(stream, directoryOffset); + // TODO: Instead, read the byte data, store for later parsing (or better yet, store offset, and read on request) + mDirectory = new EXIFReader().read(pInput); +// byte[] bom = new byte[2]; +// stream.readFully(bom); +// if (bom[0] == 'I' && bom[1] == 'I') { +// stream.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"))); +// } +// +// if (stream.readUnsignedShort() != 42) { +// throw new IIOException("Wrong TIFF magic in EXIF data."); +// } +// +// long directoryOffset = stream.readUnsignedInt(); +// +// // Read TIFF directory +// mDirectory = Directory.read(stream, directoryOffset); } @Override @@ -78,11 +78,13 @@ final class PSDEXIF1Data extends PSDImageResource { pInput.seek(pOffset); int entryCount = pInput.readUnsignedShort(); + for (int i = 0; i < entryCount; i++) { directory.mEntries.add(Entry.read(pInput)); } long nextOffset = pInput.readUnsignedInt(); + if (nextOffset != 0) { Directory next = Directory.read(pInput, nextOffset); directory.mEntries.addAll(next.mEntries); @@ -91,9 +93,9 @@ final class PSDEXIF1Data extends PSDImageResource { return directory; } - public Entry get(int pTag) { + public Entry get(int pTagId) { for (Entry entry : mEntries) { - if (entry.mTag == pTag) { + if (entry.mTagId == pTagId) { return entry; } } @@ -127,7 +129,7 @@ final class PSDEXIF1Data extends PSDImageResource { 1, 1, 2, 4, 8, 4, 8, }; - private int mTag; + final int mTagId; /* 1 = BYTE 8-bit unsigned integer. 2 = ASCII 8-bit byte that contains a 7-bit ASCII code; the last byte @@ -153,19 +155,22 @@ final class PSDEXIF1Data extends PSDImageResource { private long mValueOffset; private Object mValue; - private Entry() {} + private Entry(int pTagId) { + mTagId = pTagId; + } public static Entry read(final ImageInputStream pInput) throws IOException { - Entry entry = new Entry(); + Entry entry = new Entry(pInput.readUnsignedShort()); - entry.mTag = pInput.readUnsignedShort(); entry.mType = pInput.readShort(); entry.mCount = pInput.readInt(); // Number of values // TODO: Handle other sub-IFDs - if (entry.mTag == EXIF_IFD) { + // GPS IFD: 0x8825, Interoperability IFD: 0xA005 + if (entry.mTagId == EXIF_IFD) { long offset = pInput.readUnsignedInt(); pInput.mark(); + try { entry.mValue = Directory.read(pInput, offset); } @@ -175,6 +180,7 @@ final class PSDEXIF1Data extends PSDImageResource { } else { int valueLength = entry.getValueLength(); + if (valueLength > 0 && valueLength <= 4) { entry.readValueInLine(pInput); pInput.skipBytes(4 - valueLength); @@ -299,22 +305,21 @@ final class PSDEXIF1Data extends PSDImageResource { return -1; } - private String getTypeName() { + public final String getTypeName() { if (mType > 0 && mType <= TYPE_NAMES.length) { return TYPE_NAMES[mType - 1]; } + return "Unknown type"; } - // TODO: Tag names! - @Override - public String toString() { - return String.format("0x%04x: %s (%s, %d)", mTag, getValueAsString(), getTypeName(), mCount); + public final Object getValue() { + return mValue; } - public String getValueAsString() { + public final String getValueAsString() { if (mValue instanceof String) { - return String.format("\"%s\"", mValue); + return String.format("%s", mValue); } if (mValue != null && mValue.getClass().isArray()) { @@ -338,5 +343,11 @@ final class PSDEXIF1Data extends PSDImageResource { return String.valueOf(mValue); } + + // TODO: Tag names! + @Override + public String toString() { + return String.format("0x%04x: %s (%s, %d)", mTagId, mType == 2 ? String.format("\"%s\"", mValue) : getValueAsString(), getTypeName(), mCount); + } } } diff --git a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDGlobalLayerMask.java b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDGlobalLayerMask.java index 87e6b4b1..4f336eb1 100755 --- a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDGlobalLayerMask.java +++ b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDGlobalLayerMask.java @@ -48,17 +48,19 @@ class PSDGlobalLayerMask { final int mKind; PSDGlobalLayerMask(final ImageInputStream pInput) throws IOException { - mColorSpace = pInput.readUnsignedShort(); + mColorSpace = pInput.readUnsignedShort(); // Undocumented mColor1 = pInput.readUnsignedShort(); mColor2 = pInput.readUnsignedShort(); mColor3 = pInput.readUnsignedShort(); mColor4 = pInput.readUnsignedShort(); - mOpacity = pInput.readUnsignedShort(); + mOpacity = pInput.readUnsignedShort(); // 0-100 + + mKind = pInput.readUnsignedByte(); // 0: Selected (ie inverted), 1: Color protected, 128: Use value stored per layer + + // TODO: Variable: Filler zeros - mKind = pInput.readUnsignedByte(); - pInput.readByte(); // Pad } diff --git a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDIPTCData.java b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDIPTCData.java index 000084e7..04846e07 100644 --- a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDIPTCData.java +++ b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDIPTCData.java @@ -1,5 +1,6 @@ package com.twelvemonkeys.imageio.plugins.psd; +import com.twelvemonkeys.imageio.metadata.iptc.IPTCReader; import com.twelvemonkeys.lang.StringUtil; import javax.imageio.IIOException; @@ -7,8 +8,13 @@ import javax.imageio.stream.ImageInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.CharBuffer; -import java.nio.charset.*; -import java.util.*; +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.Iterator; +import java.util.List; /** * PSDIPTCData @@ -20,7 +26,7 @@ import java.util.*; final class PSDIPTCData extends PSDImageResource { // TODO: Refactor to be more like PSDEXIF1Data... // TODO: Extract IPTC/EXIF/XMP metadata extraction/parsing to separate module(s) - Directory mDirectory; + com.twelvemonkeys.imageio.metadata.Directory mDirectory; PSDIPTCData(final short pId, final ImageInputStream pInput) throws IOException { super(pId, pInput); @@ -28,7 +34,8 @@ final class PSDIPTCData extends PSDImageResource { @Override protected void readData(final ImageInputStream pInput) throws IOException { - mDirectory = Directory.read(pInput, mSize); + // Read IPTC directory + mDirectory = new IPTCReader().read(pInput); } @Override @@ -40,17 +47,37 @@ final class PSDIPTCData extends PSDImageResource { } static class Entry { - private int mTagId; - private String mValue; + final int mTagId; + private Object mValue; - public Entry(int pTagId, String pValue) { + public Entry(final int pTagId, final Object pValue) { mTagId = pTagId; mValue = pValue; } @Override public String toString() { - return (mTagId >> 8) + ":" + (mTagId & 0xff) + ": " + mValue; + return String.format("%d:%d: %s", mTagId >> 8, mTagId & 0xff, mValue); + } + + public final String getTypeName() { + // TODO: Should this really look like EXIF? + if (mTagId == IPTC.TAG_RECORD_VERSION) { + return "SHORT"; + } + else if (mValue instanceof String) { + return "ASCII"; + } + + return "Unknown type"; + } + + public final String getValueAsString() { + return String.valueOf(mValue); + } + + public final Object getValue() { + return mValue; } } @@ -60,6 +87,7 @@ final class PSDIPTCData extends PSDImageResource { private static final int ENCODING_UTF_8 = 0x1b2547; private int mEncoding = ENCODING_UNSPECIFIED; + final List mEntries = new ArrayList(); private Directory() {} @@ -69,6 +97,16 @@ final class PSDIPTCData extends PSDImageResource { return "Directory" + mEntries.toString(); } + public Entry get(int pTagId) { + for (Entry entry : mEntries) { + if (entry.mTagId == pTagId) { + return entry; + } + } + + return null; + } + public Iterator iterator() { return mEntries.iterator(); } @@ -81,43 +119,33 @@ final class PSDIPTCData extends PSDImageResource { // For each tag while (pInput.getStreamPosition() < streamEnd) { // Identifies start of a tag - byte b = pInput.readByte(); - if (b != 0x1c) { - throw new IIOException("Corrupt IPTC stream segment"); + byte marker = pInput.readByte(); + + if (marker != 0x1c) { + throw new IIOException(String.format("Corrupt IPTC stream segment, found 0x%02x (expected 0x1c)", marker)); } - // We need at least four bytes left to read a tag - if (pInput.getStreamPosition() + 4 >= streamEnd) { - break; - } - - int directoryType = pInput.readUnsignedByte(); - int tagType = pInput.readUnsignedByte(); + int tagId = pInput.readShort(); int tagByteCount = pInput.readUnsignedShort(); - if (pInput.getStreamPosition() + tagByteCount > streamEnd) { - throw new IIOException("Data for tag extends beyond end of IPTC segment: " + (tagByteCount + pInput.getStreamPosition() - streamEnd)); - } - - directory.processTag(pInput, directoryType, tagType, tagByteCount); + directory.readEntry(pInput, tagId, tagByteCount); } return directory; } - private void processTag(ImageInputStream pInput, int directoryType, int tagType, int tagByteCount) throws IOException { - int tagIdentifier = (directoryType << 8) | tagType; + private void readEntry(final ImageInputStream pInput, final int pTagId, final int pLength) throws IOException { + Object value = null; - String str = null; - switch (tagIdentifier) { + switch (pTagId) { case IPTC.TAG_CODED_CHARACTER_SET: - // TODO: Use this encoding!? + // TODO: Mapping from ISO 646 to Java supported character sets? // TODO: Move somewhere else? - mEncoding = parseEncoding(pInput, tagByteCount); + mEncoding = parseEncoding(pInput, pLength); return; case IPTC.TAG_RECORD_VERSION: - // short - str = Integer.toString(pInput.readUnsignedShort()); + // A single unsigned short value + value = pInput.readUnsignedShort(); break; // case IPTC.TAG_RELEASE_DATE: // case IPTC.TAG_EXPIRATION_DATE: @@ -144,50 +172,26 @@ final class PSDIPTCData extends PSDImageResource { // } // default: + // Skip non-Application fields, as they are typically not human readable + if ((pTagId & 0xff00) != IPTC.APPLICATION_RECORD) { + pInput.skipBytes(pLength); + return; + } + // fall through } - // Skip non-Application fields, as they are typically not human readable - if (directoryType << 8 != IPTC.APPLICATION_RECORD) { - return; - } - // If we don't have a value, treat it as a string - if (str == null) { - if (tagByteCount < 1) { - str = "(No value)"; + if (value == null) { + if (pLength < 1) { + value = "(No value)"; } else { - str = String.format("\"%s\"", parseString(pInput, tagByteCount)); + value = parseString(pInput, pLength); } } - mEntries.add(new Entry(tagIdentifier, str)); - -// if (directory.containsTag(tagIdentifier)) { -// // TODO: Does that REALLY help for performance?! -// // this fancy string[] business avoids using an ArrayList for performance reasons -// String[] oldStrings; -// String[] newStrings; -// try { -// oldStrings = directory.getStringArray(tagIdentifier); -// } -// catch (MetadataException e) { -// oldStrings = null; -// } -// if (oldStrings == null) { -// newStrings = new String[1]; -// } -// else { -// newStrings = new String[oldStrings.length + 1]; -// System.arraycopy(oldStrings, 0, newStrings, 0, oldStrings.length); -// } -// newStrings[newStrings.length - 1] = str; -// directory.setStringArray(tagIdentifier, newStrings); -// } -// else { -// directory.setString(tagIdentifier, str); -// } + mEntries.add(new Entry(pTagId, value)); } // private Date getDateForTime(final Directory directory, final int tagIdentifier) { @@ -267,22 +271,23 @@ final class PSDIPTCData extends PSDImageResource { // } // TODO: Pass encoding as parameter? Use if specified - private String parseString(final ImageInputStream pInput, int length) throws IOException { - // NOTE: The IPTC "spec" says ISO 646 or ISO 2022 encoding. UTF-8 contains all 646 characters, but not 2022. + 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 - // First try to decode using UTF-8 (which seems to be the de-facto standard) - String str; Charset charset = Charset.forName("UTF-8"); CharsetDecoder decoder = charset.newDecoder(); - CharBuffer chars; - byte[] data = new byte[length]; - pInput.readFully(data); + try { + // First try to decode using UTF-8 (which seems to be the de-facto standard) // Will fail fast on illegal UTF-8-sequences - chars = decoder.onMalformedInput(CodingErrorAction.REPORT) + CharBuffer chars = decoder.onMalformedInput(CodingErrorAction.REPORT) .onUnmappableCharacter(CodingErrorAction.REPORT) .decode(ByteBuffer.wrap(data)); - str = chars.toString(); + return chars.toString(); } catch (CharacterCodingException notUTF8) { if (mEncoding == ENCODING_UTF_8) { @@ -291,10 +296,8 @@ final class PSDIPTCData extends PSDImageResource { // Fall back to use ISO-8859-1 // This will not fail, but may may create wrong fallback-characters - str = StringUtil.decode(data, 0, data.length, "ISO8859_1"); + return StringUtil.decode(data, 0, data.length, "ISO8859_1"); } - - return str; } } diff --git a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java index 5eb7112e..6adb8e65 100644 --- a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java +++ b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java @@ -62,7 +62,9 @@ import java.util.List; * @version $Id: PSDImageReader.java,v 1.0 Apr 29, 2008 4:45:52 PM haraldk Exp$ */ // TODO: Implement ImageIO meta data interface -// TODO: API for reading separate layers +// TODO: Allow reading the extra alpha channels (index after composite data) +// TODO: Support for PSDVersionInfo hasRealMergedData=false (no real composite data, layers will be in index 0) +// TODO: Support for API for reading separate layers (index after composite data, and optional alpha channels) // TODO: Consider Romain Guy's Java 2D implementation of PS filters for the blending modes in layers // http://www.curious-creature.org/2006/09/20/new-blendings-modes-for-java2d/ // See http://www.codeproject.com/KB/graphics/PSDParser.aspx @@ -1144,11 +1146,12 @@ public class PSDImageReader extends ImageReaderBase { node = metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); serializer = new XMLSerializer(System.out, System.getProperty("file.encoding")); + serializer.setIndentation(" "); serializer.serialize(node, true); System.out.println(); node = metadata.getAsTree(PSDMetadata.NATIVE_METADATA_FORMAT_NAME); - serializer = new XMLSerializer(System.out, System.getProperty("file.encoding")); +// serializer = new XMLSerializer(System.out, System.getProperty("file.encoding")); serializer.serialize(node, true); if (imageReader.hasThumbnails(0)) { diff --git a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageResource.java b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageResource.java index 180ce4d7..7aa60dc4 100644 --- a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageResource.java +++ b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageResource.java @@ -28,6 +28,7 @@ package com.twelvemonkeys.imageio.plugins.psd; +import com.twelvemonkeys.imageio.stream.SubImageInputStream; import com.twelvemonkeys.lang.StringUtil; import javax.imageio.stream.ImageInputStream; @@ -62,11 +63,16 @@ class PSDImageResource { } mSize = pInput.readUnsignedInt(); - readData(pInput); + long startPos = pInput.getStreamPosition(); - // TODO: Sanity check reading here? + readData(new SubImageInputStream(pInput, mSize)); - // Data is even-padded + // NOTE: This should never happen, however it's safer to keep it here to + if (pInput.getStreamPosition() != startPos + mSize) { + pInput.seek(startPos + mSize); + } + + // Data is even-padded (word aligned) if (mSize % 2 != 0) { pInput.read(); } diff --git a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java index e622bc01..276d7e07 100644 --- a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java +++ b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadata.java @@ -1,14 +1,13 @@ package com.twelvemonkeys.imageio.plugins.psd; +import com.twelvemonkeys.imageio.metadata.Directory; +import com.twelvemonkeys.imageio.metadata.Entry; 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.IIOInvalidTreeException; -import javax.imageio.metadata.IIOMetadata; -import javax.imageio.metadata.IIOMetadataFormatImpl; import javax.imageio.metadata.IIOMetadataNode; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -24,7 +23,7 @@ import java.util.List; * @author last modified by $Author: haraldk$ * @version $Id: PSDMetadata.java,v 1.0 Nov 4, 2009 5:28:12 PM haraldk Exp$ */ -public final class PSDMetadata extends IIOMetadata implements Cloneable { +public final class PSDMetadata extends AbstractMetadata { // TODO: Decide on image/stream metadata... static final String NATIVE_METADATA_FORMAT_NAME = "com_twelvemonkeys_imageio_psd_image_1.0"; @@ -60,98 +59,15 @@ public final class PSDMetadata extends IIOMetadata implements Cloneable { static final String[] PRINT_SCALE_STYLES = {"centered", "scaleToFit", "userDefined"}; - protected PSDMetadata() { // TODO: Allow XMP, EXIF and IPTC as extra formats? super(true, NATIVE_METADATA_FORMAT_NAME, NATIVE_METADATA_FORMAT_CLASS_NAME, null, null); } - @Override - public boolean isReadOnly() { - // TODO: Extract to abstract metadata impl class? - return true; - } - - @Override - public Node getAsTree(final String pFormatName) { - validateFormatName(pFormatName); - - if (pFormatName.equals(nativeMetadataFormatName)) { - return getNativeTree(); - } - else if (pFormatName.equals(IIOMetadataFormatImpl.standardMetadataFormatName)) { - return getStandardTree(); - } - - throw new AssertionError("Unreachable"); - } - - @Override - public void mergeTree(final String pFormatName, final Node pRoot) throws IIOInvalidTreeException { - // TODO: Extract to abstract metadata impl class? - assertMutable(); - - validateFormatName(pFormatName); - - if (!pRoot.getNodeName().equals(nativeMetadataFormatName)) { - throw new IIOInvalidTreeException("Root must be " + nativeMetadataFormatName, pRoot); - } - - Node node = pRoot.getFirstChild(); - while (node != null) { - // TODO: Merge values from node into this - - // Move to the next sibling - node = node.getNextSibling(); - } - } - - @Override - public void reset() { - // TODO: Extract to abstract metadata impl class? - assertMutable(); - - throw new UnsupportedOperationException("Method reset not implemented"); // TODO: Implement - } - - // TODO: Extract to abstract metadata impl class? - private void assertMutable() { - if (isReadOnly()) { - throw new IllegalStateException("Metadata is read-only"); - } - } - - // TODO: Extract to abstract metadata impl class? - private void validateFormatName(final String pFormatName) { - String[] metadataFormatNames = getMetadataFormatNames(); - - if (metadataFormatNames != null) { - for (String metadataFormatName : metadataFormatNames) { - if (metadataFormatName.equals(pFormatName)) { - return; // Found, we're ok! - } - } - } - - throw new IllegalArgumentException( - String.format("Bad format name: \"%s\". Expected one of %s", pFormatName, Arrays.toString(metadataFormatNames)) - ); - } - - @Override - public Object clone() { - // TODO: Make it a deep clone - try { - return super.clone(); - } - catch (CloneNotSupportedException e) { - throw new RuntimeException(e); - } - } - /// Native format support - private Node getNativeTree() { + @Override + protected Node getNativeTree() { IIOMetadataNode root = new IIOMetadataNode(NATIVE_METADATA_FORMAT_NAME); root.appendChild(createHeaderNode()); @@ -195,6 +111,18 @@ public final class PSDMetadata extends IIOMetadata implements Cloneable { // TODO: Format spec node = new IIOMetadataNode("ICCProfile"); node.setAttribute("colorSpaceType", JAVA_CS[profile.getProfile().getColorSpaceType()]); +// +// FastByteArrayOutputStream data = new FastByteArrayOutputStream(0); +// EncoderStream base64 = new EncoderStream(data, new Base64Encoder(), true); +// +// try { +// base64.write(profile.getProfile().getData()); +// } +// catch (IOException ignore) { +// } +// +// byte[] bytes = data.toByteArray(); +// node.setAttribute("data", StringUtil.decode(bytes, 0, bytes.length, "ASCII")); node.setUserObject(profile.getProfile()); } else if (imageResource instanceof PSDAlphaChannelInfo) { @@ -215,10 +143,12 @@ public final class PSDMetadata extends IIOMetadata implements Cloneable { node.setAttribute("colorSpace", DISPLAY_INFO_CS[displayInfo.mColorSpace]); StringBuilder builder = new StringBuilder(); + for (short color : displayInfo.mColors) { if (builder.length() > 0) { builder.append(" "); } + builder.append(Integer.toString(color)); } @@ -324,30 +254,65 @@ public final class PSDMetadata extends IIOMetadata implements Cloneable { // Transcode to XMP? ;-) PSDIPTCData iptc = (PSDIPTCData) imageResource; - node = new IIOMetadataNode("IPTC"); + node = new IIOMetadataNode("Directory"); + 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); + } } else if (imageResource instanceof PSDEXIF1Data) { // TODO: Revise/rethink this... // Transcode to XMP? ;-) PSDEXIF1Data exif = (PSDEXIF1Data) imageResource; - node = new IIOMetadataNode("EXIF"); + node = new IIOMetadataNode("Directory"); + node.setAttribute("type", "EXIF"); + // TODO: Set byte[] data instead node.setUserObject(exif.mDirectory); + + appendEntries(node, 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(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(new InputSource(xmp.getData())); + 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(); @@ -355,7 +320,13 @@ public final class PSDMetadata extends IIOMetadata implements Cloneable { } else { // Generic resource.. - node = new IIOMetadataNode(PSDImageResource.resourceTypeForId(imageResource.mId)); + node = new IIOMetadataNode("ImageResource"); + String value = PSDImageResource.resourceTypeForId(imageResource.mId); + if (!"UnknownResource".equals(value)) { + node.setAttribute("name", value); + } + node.setAttribute("length", String.valueOf(imageResource.mSize)); + // TODO: Set user object: byte array } // TODO: More resources @@ -364,9 +335,36 @@ public final class PSDMetadata extends IIOMetadata implements Cloneable { resource.appendChild(node); } + // TODO: Layers and layer info + + // TODO: Global mask etc.. + return resource; } + private void appendEntries(IIOMetadataNode pNode, final Directory pDirectory) { + for (Entry entry : pDirectory) { + IIOMetadataNode tag = new IIOMetadataNode("Entry"); + tag.setAttribute("tag", String.format("%s", entry.getIdentifier())); + + String field = entry.getFieldName(); + if (field != null) { + tag.setAttribute("field", String.format("%s", field)); + } + + if (entry.getValue() instanceof Directory) { + appendEntries(tag, (Directory) entry.getValue()); + tag.setAttribute("type", "Directory"); + } + else { + tag.setAttribute("value", entry.getValueAsString()); + tag.setAttribute("type", entry.getTypeName()); + } + + pNode.appendChild(tag); + } + } + /// Standard format support @Override @@ -461,7 +459,7 @@ public final class PSDMetadata extends IIOMetadata implements Cloneable { private String getMultiChannelCS(short pChannels) { if (pChannels < 16) { - return Integer.toHexString(pChannels) + "CLR"; + return String.format("%xCLR", pChannels); } throw new UnsupportedOperationException("Standard meta data format does not support more than 15 channels"); @@ -469,88 +467,101 @@ public final class PSDMetadata extends IIOMetadata implements Cloneable { @Override protected IIOMetadataNode getStandardCompressionNode() { - IIOMetadataNode compression_node = new IIOMetadataNode("Compression"); + IIOMetadataNode compressionNode = new IIOMetadataNode("Compression"); IIOMetadataNode node; // scratch node node = new IIOMetadataNode("CompressionTypeName"); String compression; + switch (mCompression) { case PSD.COMPRESSION_NONE: compression = "none"; break; case PSD.COMPRESSION_RLE: - compression = "packbits"; + compression = "PackBits"; break; case PSD.COMPRESSION_ZIP: case PSD.COMPRESSION_ZIP_PREDICTION: - compression = "zip"; + compression = "Deflate"; // TODO: ZLib? (TIFF native metadata format specifies both.. :-P) break; default: throw new AssertionError("Unreachable"); } - node.setAttribute("value", compression); - compression_node.appendChild(node); + node.setAttribute("value", compression); + compressionNode.appendChild(node); + + // TODO: Does it make sense to specify lossless for compression "none"? node = new IIOMetadataNode("Lossless"); node.setAttribute("value", "true"); - compression_node.appendChild(node); + compressionNode.appendChild(node); - return compression_node; + return compressionNode; } @Override protected IIOMetadataNode getStandardDataNode() { - IIOMetadataNode data_node = new IIOMetadataNode("Data"); + IIOMetadataNode dataNode = new IIOMetadataNode("Data"); IIOMetadataNode node; // scratch node node = new IIOMetadataNode("PlanarConfiguration"); node.setAttribute("value", "PlaneInterleaved"); // TODO: Check with spec - data_node.appendChild(node); + dataNode.appendChild(node); node = new IIOMetadataNode("SampleFormat"); node.setAttribute("value", mHeader.mMode == PSD.COLOR_MODE_INDEXED ? "Index" : "UnsignedIntegral"); - data_node.appendChild(node); + dataNode.appendChild(node); String bitDepth = Integer.toString(mHeader.mBits); // bits per plane + // TODO: Channels might be 5 for RGB + A + Mask... String[] bps = new String[mHeader.mChannels]; Arrays.fill(bps, bitDepth); node = new IIOMetadataNode("BitsPerSample"); node.setAttribute("value", StringUtil.toCSVString(bps, " ")); - data_node.appendChild(node); + dataNode.appendChild(node); // TODO: SampleMSB? Or is network (aka Motorola/big endian) byte order assumed? - return data_node; + return dataNode; } @Override protected IIOMetadataNode getStandardDimensionNode() { - IIOMetadataNode dimension_node = new IIOMetadataNode("Dimension"); + IIOMetadataNode dimensionNode = new IIOMetadataNode("Dimension"); IIOMetadataNode node; // scratch node node = new IIOMetadataNode("PixelAspectRatio"); - // TODO: This is not incorrect wrt resolution info - float ratio = 1f; - node.setAttribute("value", Float.toString(ratio)); - dimension_node.appendChild(node); + + // TODO: This is not correct wrt resolution info + float aspect = 1f; + + Iterator ratios = getResources(PSDPixelAspectRatio.class); + if (ratios.hasNext()) { + PSDPixelAspectRatio ratio = ratios.next(); + aspect = (float) ratio.mAspect; + } + + node.setAttribute("value", Float.toString(aspect)); + dimensionNode.appendChild(node); node = new IIOMetadataNode("ImageOrientation"); node.setAttribute("value", "Normal"); - dimension_node.appendChild(node); + dimensionNode.appendChild(node); + // TODO: If no PSDResolutionInfo, this might still be available in the EXIF data... Iterator resolutionInfos = getResources(PSDResolutionInfo.class); if (!resolutionInfos.hasNext()) { PSDResolutionInfo resolutionInfo = resolutionInfos.next(); node = new IIOMetadataNode("HorizontalPixelSize"); node.setAttribute("value", Float.toString(asMM(resolutionInfo.mHResUnit, resolutionInfo.mHRes))); - dimension_node.appendChild(node); + dimensionNode.appendChild(node); node = new IIOMetadataNode("VerticalPixelSize"); node.setAttribute("value", Float.toString(asMM(resolutionInfo.mVResUnit, resolutionInfo.mVRes))); - dimension_node.appendChild(node); + dimensionNode.appendChild(node); } // TODO: @@ -580,7 +591,7 @@ public final class PSDMetadata extends IIOMetadata implements Cloneable { */ - return dimension_node; + return dimensionNode; } private static float asMM(final short pUnit, final float pResolution) { @@ -603,18 +614,18 @@ public final class PSDMetadata extends IIOMetadata implements Cloneable { PSDEXIF1Data data = exif.next(); // Get the EXIF DateTime (aka ModifyDate) tag if present - PSDEXIF1Data.Entry dateTime = data.mDirectory.get(0x0132); // TODO: Constant + Entry dateTime = data.mDirectory.getEntryById(0x0132); // TODO: Constant if (dateTime != null) { - node = new IIOMetadataNode("ImageModificationTime"); - // Format: "YYYY:MM:DD hh:mm:ss" (with quotes! :-P) + node = new IIOMetadataNode("ImageCreationTime"); // As TIFF, but could just as well be ImageModificationTime + // Format: "YYYY:MM:DD hh:mm:ss" String value = dateTime.getValueAsString(); - node.setAttribute("year", value.substring(1, 5)); - node.setAttribute("month", value.substring(6, 8)); - node.setAttribute("day", value.substring(9, 11)); - node.setAttribute("hour", value.substring(12, 14)); - node.setAttribute("minute", value.substring(15, 17)); - node.setAttribute("second", value.substring(18, 20)); + node.setAttribute("year", value.substring(0, 4)); + node.setAttribute("month", value.substring(5, 7)); + node.setAttribute("day", value.substring(8, 10)); + node.setAttribute("hour", value.substring(11, 13)); + node.setAttribute("minute", value.substring(14, 16)); + node.setAttribute("second", value.substring(17, 19)); document_node.appendChild(node); } @@ -625,61 +636,68 @@ public final class PSDMetadata extends IIOMetadata implements Cloneable { @Override protected IIOMetadataNode getStandardTextNode() { - // TODO: CaptionDigest?, EXIF, XMP + // TODO: TIFF uses + // DocumentName, ImageDescription, Make, Model, PageName, Software, Artist, HostComputer, InkNames, Copyright: + // /Text/TextEntry@keyword = field name, /Text/TextEntry@value = field value. + // 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 textResources = getResources(PSDEXIF1Data.class, PSDXMPData.class); + Iterator textResources = getResources(PSDEXIF1Data.class, PSDIPTCData.class, PSDXMPData.class); + + if (!textResources.hasNext()) { + return null; + } + + IIOMetadataNode text = new IIOMetadataNode("Text"); + IIOMetadataNode node; + + // TODO: Alpha channel names? (PSDAlphaChannelInfo/PSDUnicodeAlphaNames) + // TODO: Reader/writer (PSDVersionInfo) while (textResources.hasNext()) { PSDImageResource textResource = textResources.next(); - } - -// int numEntries = tEXt_keyword.size() + -// iTXt_keyword.size() + zTXt_keyword.size(); -// if (numEntries == 0) { -// return null; -// } -// -// IIOMetadataNode text_node = new IIOMetadataNode("Text"); -// IIOMetadataNode node = null; // scratch node -// -// for (int i = 0; i < tEXt_keyword.size(); i++) { -// node = new IIOMetadataNode("TextEntry"); -// node.setAttribute("keyword", (String)tEXt_keyword.get(i)); -// node.setAttribute("value", (String)tEXt_text.get(i)); -// node.setAttribute("encoding", "ISO-8859-1"); -// node.setAttribute("compression", "none"); -// -// text_node.appendChild(node); -// } -// -// for (int i = 0; i < iTXt_keyword.size(); i++) { -// node = new IIOMetadataNode("TextEntry"); -// node.setAttribute("keyword", iTXt_keyword.get(i)); -// node.setAttribute("value", iTXt_text.get(i)); -// node.setAttribute("language", -// iTXt_languageTag.get(i)); -// if (iTXt_compressionFlag.get(i)) { -// node.setAttribute("compression", "deflate"); -// } else { -// node.setAttribute("compression", "none"); -// } -// -// text_node.appendChild(node); -// } -// -// for (int i = 0; i < zTXt_keyword.size(); i++) { -// node = new IIOMetadataNode("TextEntry"); -// node.setAttribute("keyword", (String)zTXt_keyword.get(i)); -// node.setAttribute("value", (String)zTXt_text.get(i)); -// node.setAttribute("compression", "deflate"); -// -// text_node.appendChild(node); -// } -// -// return text_node; - return null; + if (textResource instanceof PSDIPTCData) { + PSDIPTCData iptc = (PSDIPTCData) textResource; + for (Entry entry : iptc.mDirectory) { + node = new IIOMetadataNode("TextEntry"); + + if (entry.getValue() instanceof String) { + node.setAttribute("keyword", String.format("%s", entry.getFieldName())); + node.setAttribute("value", entry.getValueAsString()); + text.appendChild(node); + } + } + } + else if (textResource instanceof PSDEXIF1Data) { + PSDEXIF1Data exif = (PSDEXIF1Data) textResource; + + // TODO: Use name? + appendTextEntriesFlat(text, exif.mDirectory); + } + else if (textResource instanceof PSDXMPData) { + // TODO: Parse XMP (heavy) ONLY if we don't have required fields from IPTC/EXIF? + PSDXMPData xmp = (PSDXMPData) textResource; + } + } + + return text; + } + + private void appendTextEntriesFlat(IIOMetadataNode pNode, Directory pDirectory) { + for (Entry entry : pDirectory) { + if (entry.getValue() instanceof Directory) { + appendTextEntriesFlat(pNode, (Directory) entry.getValue()); + } + else if (entry.getValue() instanceof String) { + IIOMetadataNode tag = new IIOMetadataNode("TextEntry"); + // TODO: Use name! + tag.setAttribute("keyword", String.format("%s", entry.getFieldName())); + tag.setAttribute("value", entry.getValueAsString()); + pNode.appendChild(tag); + } + } } @Override @@ -693,7 +711,7 @@ public final class PSDMetadata extends IIOMetadata implements Cloneable { IIOMetadataNode node; // scratch node node = new IIOMetadataNode("Alpha"); - node.setAttribute("value", hasAlpha() ? "nonpremultipled" : "none"); // TODO: Check spec + node.setAttribute("value", hasAlpha() ? "nonpremultiplied" : "none"); // TODO: Check spec transparency_node.appendChild(node); return transparency_node; @@ -731,4 +749,15 @@ public final class PSDMetadata extends IIOMetadata implements Cloneable { } }); } + + @Override + public Object clone() { + // TODO: Make it a deep clone + try { + return super.clone(); + } + catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } } diff --git a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadataFormat.java b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadataFormat.java index 38ac1276..b842ad3b 100644 --- a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadataFormat.java +++ b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDMetadataFormat.java @@ -44,7 +44,6 @@ public final class PSDMetadataFormat extends IIOMetadataFormatImpl { // columns? addAttribute("Header", "width", DATATYPE_INTEGER, true, null, "1", "30000", true, true); addAttribute("Header", "bits", DATATYPE_INTEGER, true, null, Arrays.asList("1", "8", "16")); - // TODO: Consider using more readable names?! addAttribute("Header", "mode", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.COLOR_MODES)); /* diff --git a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDUnicodeAlphaNames.java b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDUnicodeAlphaNames.java index 12c927e9..6901a8a3 100644 --- a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDUnicodeAlphaNames.java +++ b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDUnicodeAlphaNames.java @@ -25,7 +25,7 @@ final class PSDUnicodeAlphaNames extends PSDImageResource { long left = mSize; while (left > 0) { - String name = PSDUtil.readUTF16String(pInput); + String name = PSDUtil.readUnicodeString(pInput); mNames.add(name); left -= name.length() * 2 + 4; } diff --git a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDUtil.java b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDUtil.java index f3e1ab41..9f840406 100644 --- a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDUtil.java +++ b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDUtil.java @@ -60,17 +60,27 @@ final class PSDUtil { } // TODO: Proably also useful for PICT reader, move to some common util? - // TODO: Is this REALLY different from the previous method? Maybe the pad should not be read.. static String readPascalString(final DataInput pInput) throws IOException { int length = pInput.readUnsignedByte(); + + if (length == 0) { + return ""; + } + byte[] bytes = new byte[length]; pInput.readFully(bytes); return StringUtil.decode(bytes, 0, bytes.length, "ASCII"); } - static String readUTF16String(final DataInput pInput) throws IOException { + // TODO: Proably also useful for PICT reader, move to some common util? + static String readUnicodeString(final DataInput pInput) throws IOException { int length = pInput.readInt(); + + if (length == 0) { + return ""; + } + byte[] bytes = new byte[length * 2]; pInput.readFully(bytes); diff --git a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDVersionInfo.java b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDVersionInfo.java index 683fa3a3..5e1e7ea2 100644 --- a/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDVersionInfo.java +++ b/twelvemonkeys-imageio/psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDVersionInfo.java @@ -35,8 +35,8 @@ final class PSDVersionInfo extends PSDImageResource { mVersion = pInput.readInt(); mHasRealMergedData = pInput.readBoolean(); - mWriter = PSDUtil.readUTF16String(pInput); - mReader = PSDUtil.readUTF16String(pInput); + mWriter = PSDUtil.readUnicodeString(pInput); + mReader = PSDUtil.readUnicodeString(pInput); mFileVersion = pInput.readInt(); }