Merge commit 'upstream/master'

This commit is contained in:
Erlend Hamnaberg 2009-11-11 21:47:48 +01:00
commit ec4334cbb5
37 changed files with 1920 additions and 151 deletions

View File

@ -60,6 +60,7 @@ public final class DecoderStream extends FilterInputStream {
* @see java.io.FilterInputStream#in * @see java.io.FilterInputStream#in
*/ */
public DecoderStream(final InputStream pStream, final Decoder pDecoder) { public DecoderStream(final InputStream pStream, final Decoder pDecoder) {
// TODO: Let the decoder decide preferred buffer size
this(pStream, pDecoder, 1024); this(pStream, pDecoder, 1024);
} }

View File

@ -107,6 +107,7 @@ public final class PackBitsDecoder implements Decoder {
int read = 0; int read = 0;
final int max = pBuffer.length; final int max = pBuffer.length;
// TODO: Don't decode more than single runs, because some writers add pad bytes inside the stream...
while (read < max) { while (read < max) {
int n; int n;

View File

@ -1572,7 +1572,7 @@ public final class StringUtil {
* @param pStringArray the string array * @param pStringArray the string array
* @return A string of comma-separated values * @return A string of comma-separated values
*/ */
public static String toCSVString(String[] pStringArray) { public static String toCSVString(Object[] pStringArray) {
return toCSVString(pStringArray, ", "); return toCSVString(pStringArray, ", ");
} }
@ -1584,7 +1584,7 @@ public final class StringUtil {
* @return string of delimiter separated values * @return string of delimiter separated values
* @throws IllegalArgumentException if {@code pDelimiterString == null} * @throws IllegalArgumentException if {@code pDelimiterString == null}
*/ */
public static String toCSVString(String[] pStringArray, String pDelimiterString) { public static String toCSVString(Object[] pStringArray, String pDelimiterString) {
if (pStringArray == null) { if (pStringArray == null) {
return ""; return "";
} }

View File

@ -115,13 +115,7 @@ public class BASE64 {
return buf.toString(); return buf.toString();
} }
/** public static byte[] decode(String pData) throws IOException {
* Quick implementation, using the undocumented
* {@code sun.misc.BASE64Decoder.decodeBuffer(String)}.
*/
public static byte[] decode(String pData) throws java.io.IOException {
//return DECODER.decodeBuffer(pData);
InputStream in = new DecoderStream(new ByteArrayInputStream(pData.getBytes()), new Base64Decoder()); InputStream in = new DecoderStream(new ByteArrayInputStream(pData.getBytes()), new Base64Decoder());
ByteArrayOutputStream bytes = new FastByteArrayOutputStream(pData.length() * 3); ByteArrayOutputStream bytes = new FastByteArrayOutputStream(pData.length() * 3);
FileUtil.copy(in, bytes); FileUtil.copy(in, bytes);
@ -131,7 +125,7 @@ public class BASE64 {
//private final static sun.misc.BASE64Decoder DECODER = new sun.misc.BASE64Decoder(); //private final static sun.misc.BASE64Decoder DECODER = new sun.misc.BASE64Decoder();
public static void main(String[] pArgs) throws java.io.IOException { public static void main(String[] pArgs) throws IOException {
if (pArgs.length == 1) { if (pArgs.length == 1) {
System.out.println(encode(pArgs[0].getBytes())); System.out.println(encode(pArgs[0].getBytes()));
} }

View File

@ -0,0 +1,4 @@
/**
* Provides helper classes for service provider implementations.
*/
package com.twelvemonkeys.imageio.spi;

View File

@ -0,0 +1,4 @@
/**
* Provides various additional stream implementations.
*/
package com.twelvemonkeys.imageio.stream;

View File

@ -0,0 +1,4 @@
/**
* Provides various common utilities for reading and writing images.
*/
package com.twelvemonkeys.imageio.util;

View File

@ -36,6 +36,7 @@ import org.jmock.core.Stub;
import javax.imageio.*; import javax.imageio.*;
import javax.imageio.event.IIOReadProgressListener; import javax.imageio.event.IIOReadProgressListener;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.IIORegistry; import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
@ -1320,7 +1321,9 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> extends
assertEquals(type.getColorModel(), result.getColorModel()); assertEquals(type.getColorModel(), result.getColorModel());
// assertEquals(type.getSampleModel(), result.getSampleModel()); // The following logically tests
// assertEquals(type.getSampleModel(), result.getSampleModel());
// but SampleModel does not have a proper equals method.
SampleModel expectedModel = type.getSampleModel(); SampleModel expectedModel = type.getSampleModel();
SampleModel resultModel = result.getSampleModel(); SampleModel resultModel = result.getSampleModel();
@ -1335,10 +1338,6 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> extends
} }
} }
// public void testSetDestinationTypeIllegal() throws IOException {
// throw new UnsupportedOperationException("Method testSetDestinationTypeIllegal not implemented"); // TODO: Implement
// }
//
// public void testSetDestinationBands() throws IOException { // public void testSetDestinationBands() throws IOException {
// throw new UnsupportedOperationException("Method testSetDestinationBands not implemented"); // TODO: Implement // throw new UnsupportedOperationException("Method testSetDestinationBands not implemented"); // TODO: Implement
// } // }
@ -1347,6 +1346,24 @@ public abstract class ImageReaderAbstractTestCase<T extends ImageReader> extends
// throw new UnsupportedOperationException("Method testSetDestinationBands not implemented"); // TODO: Implement // throw new UnsupportedOperationException("Method testSetDestinationBands not implemented"); // TODO: Implement
// } // }
public void testProviderAndMetadataFormatNamesMatch() throws IOException {
ImageReaderSpi provider = createProvider();
ImageReader reader = createReader();
reader.setInput(getTestData().get(0).getInputStream());
IIOMetadata imageMetadata = reader.getImageMetadata(0);
if (imageMetadata != null) {
assertEquals(provider.getNativeImageMetadataFormatName(), imageMetadata.getNativeMetadataFormatName());
}
IIOMetadata streamMetadata = reader.getStreamMetadata();
if (streamMetadata != null) {
assertEquals(provider.getNativeStreamMetadataFormatName(), streamMetadata.getNativeMetadataFormatName());
}
}
protected URL getClassLoaderResource(final String pName) { protected URL getClassLoaderResource(final String pName) {
return getClass().getResource(pName); return getClass().getResource(pName);
} }

View File

@ -94,13 +94,7 @@ public class IFFImageReader extends ImageReaderBase {
// http://home.comcast.net/~erniew/lwsdk/docs/filefmts/ilbm.html // http://home.comcast.net/~erniew/lwsdk/docs/filefmts/ilbm.html
// http://www.fileformat.info/format/iff/spec/7866a9f0e53c42309af667c5da3bd426/view.htm // http://www.fileformat.info/format/iff/spec/7866a9f0e53c42309af667c5da3bd426/view.htm
// - Contains definitions of some "new" chunks, as well as alternative FORM types // - Contains definitions of some "new" chunks, as well as alternative FORM types
// http://amigan.1emu.net/reg/iff.html
// TODO: One other existing deep bit ordering that you may encounter is the 21-bit
// NewTek format.
//
// NewTek deep ILBM bit ordering:
// saved first ------------------------------------------------------> saved last
// R7 G7 B7 R6 G6 B6 R5 G5 B5 R4 G4 B4 R3 G3 B3 R2 G2 B2 R1 G1 B1 R0 G0 B0
private BMHDChunk mHeader; private BMHDChunk mHeader;
private CMAPChunk mColorMap; private CMAPChunk mColorMap;

View File

@ -175,7 +175,7 @@ public class IFFImageWriter extends ImageWriterBase {
private void writeMeta(RenderedImage pImage, int pBodyLength) throws IOException { private void writeMeta(RenderedImage pImage, int pBodyLength) throws IOException {
// Annotation ANNO chunk, 8 + annoData.length bytes // Annotation ANNO chunk, 8 + annoData.length bytes
String annotation = "Written by " + getOriginatingProvider().getDescription(null); String annotation = "Written by " + getOriginatingProvider().getDescription(null) + " by " + getOriginatingProvider().getVendorName();
GenericChunk anno = new GenericChunk(IFFUtil.toInt("ANNO".getBytes()), annotation.getBytes()); GenericChunk anno = new GenericChunk(IFFUtil.toInt("ANNO".getBytes()), annotation.getBytes());
ColorModel cm = pImage.getColorModel(); ColorModel cm = pImage.getColorModel();

View File

@ -113,16 +113,11 @@
</plugin> </plugin>
<plugin> <plugin>
<!-- TODO: Make default -->
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-jar-plugin</artifactId>
<version>2.2</version> <version>2.2</version>
<configuration> <configuration>
<archive> <archive>
<manifest>
<!--<mainClass>com.mycompany.app.App</mainClass>-->
<!-- <packageName>com.twelvemonkeys.imageio.plugins.psd</packageName> -->
</manifest>
<manifestEntries> <manifestEntries>
<Implementation-Title>${project.name}</Implementation-Title> <Implementation-Title>${project.name}</Implementation-Title>
<Implementation-Vendor>TwelveMonkeys</Implementation-Vendor> <Implementation-Vendor>TwelveMonkeys</Implementation-Vendor>

View File

@ -101,7 +101,7 @@ interface PSD {
int COMPRESSION_ZIP = 2; int COMPRESSION_ZIP = 2;
/** ZIP compression with prediction */ /** ZIP compression with prediction */
int COMPRESSION_ZIP_PREDICTON = 3; int COMPRESSION_ZIP_PREDICTION = 3;
// Color Modes // Color Modes
/** Bitmap (monochrome) */ /** Bitmap (monochrome) */
@ -225,11 +225,11 @@ interface PSD {
// 03f8 // 03f8
/** Color transfer functions */ /** Color transfer functions */
int RES_COLOR_TRANSFER_FUNCITON = 0x03f8; int RES_COLOR_TRANSFER_FUNCTION = 0x03f8;
// 03f9 // 03f9
/** Duotone transfer functions */ /** Duotone transfer functions */
int RES_DUOTONE_TRANSFER_FUNCITON = 0x03f9; int RES_DUOTONE_TRANSFER_FUNCTOON = 0x03f9;
// 03fa // 03fa
/** Duotone image information */ /** Duotone image information */
@ -385,7 +385,7 @@ interface PSD {
* (Photoshop 5.0) Unicode Alpha Names * (Photoshop 5.0) Unicode Alpha Names
* Unicode string (4 bytes length followed by string). * Unicode string (4 bytes length followed by string).
*/ */
int RES_UNICODE_ALPHA_NAME = 0x0415; int RES_UNICODE_ALPHA_NAMES = 0x0415;
// 1046 // 1046
/** /**

View File

@ -48,11 +48,11 @@ class PSDAlphaChannelInfo extends PSDImageResource {
} }
@Override @Override
protected void readData(ImageInputStream pInput) throws IOException { protected void readData(final ImageInputStream pInput) throws IOException {
mNames = new ArrayList<String>(); mNames = new ArrayList<String>();
long left = mSize; long left = mSize;
while (left > 0) { while (left > 0) {
String name = PSDUtil.readPascalStringByte(pInput); String name = PSDUtil.readPascalString(pInput);
mNames.add(name); mNames.add(name);
left -= name.length() + 1; left -= name.length() + 1;
} }

View File

@ -47,7 +47,7 @@ class PSDColorData {
final byte[] mColors; final byte[] mColors;
private IndexColorModel mColorModel; private IndexColorModel mColorModel;
PSDColorData(ImageInputStream pInput) throws IOException { PSDColorData(final ImageInputStream pInput) throws IOException {
int length = pInput.readInt(); int length = pInput.readInt();
if (length == 0) { if (length == 0) {
throw new IIOException("No palette information in PSD"); throw new IIOException("No palette information in PSD");
@ -72,7 +72,7 @@ class PSDColorData {
return mColorModel; return mColorModel;
} }
private int[] toInterleavedRGB(byte[] pColors) { private static int[] toInterleavedRGB(final byte[] pColors) {
int[] rgb = new int[pColors.length / 3]; int[] rgb = new int[pColors.length / 3];
for (int i = 0; i < rgb.length; i++) { for (int i = 0; i < rgb.length; i++) {

View File

@ -40,7 +40,22 @@ import java.io.IOException;
* @version $Id: PSDResolutionInfo.java,v 1.0 May 2, 2008 3:58:19 PM haraldk Exp$ * @version $Id: PSDResolutionInfo.java,v 1.0 May 2, 2008 3:58:19 PM haraldk Exp$
*/ */
class PSDDisplayInfo extends PSDImageResource { class PSDDisplayInfo extends PSDImageResource {
// TODO: Size of this struct should be 14.. Does not compute... // TODO: Size of this struct should be 14.. Does not compute... Something bogus here
// ColorSpace definitions:
// PSD_CS_RGB = 0, /* RGB */
// PSD_CS_HSB = 1, /* Hue, Saturation, Brightness */
// PSD_CS_CMYK = 2, /* CMYK */
// PSD_CS_PANTONE = 3, /* Pantone matching system (Lab)*/
// PSD_CS_FOCOLTONE = 4, /* Focoltone colour system (CMYK)*/
// PSD_CS_TRUMATCH = 5, /* Trumatch color (CMYK)*/
// PSD_CS_TOYO = 6, /* Toyo 88 colorfinder 1050 (Lab)*/
// PSD_CS_LAB = 7, /* L*a*b*/
// PSD_CS_GRAYSCALE = 8, /* Grey scale */
// PSD_CS_HKS = 10, /* HKS colors (CMYK)*/
// PSD_CS_DIC = 11, /* DIC color guide (Lab)*/
// PSD_CS_ANPA = 3000, /* Anpa color (Lab)*/
//typedef _DisplayInfo //typedef _DisplayInfo
//{ //{
// WORD ColorSpace; // WORD ColorSpace;
@ -50,10 +65,10 @@ class PSDDisplayInfo extends PSDImageResource {
// BYTE Padding; /* Always zero */ // BYTE Padding; /* Always zero */
//} DISPLAYINFO; //} DISPLAYINFO;
private int mColorSpace; int mColorSpace;
private short[] mColors; short[] mColors;
private short mOpacity; short mOpacity;
private byte mKind; byte mKind;
PSDDisplayInfo(final short pId, final ImageInputStream pInput) throws IOException { PSDDisplayInfo(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput); super(pId, pInput);

View File

@ -10,6 +10,7 @@ import java.io.IOException;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator;
import java.util.List; import java.util.List;
/** /**
@ -35,7 +36,7 @@ final class PSDEXIF1Data extends PSDImageResource {
protected void readData(final ImageInputStream pInput) throws IOException { protected void readData(final ImageInputStream pInput) throws IOException {
// This is in essence an embedded TIFF file. // This is in essence an embedded TIFF file.
// TODO: Extract TIFF parsing to more general purpose package // TODO: Extract TIFF parsing to more general purpose package
// TODO: Instead, read the byte data, store for later parsing // 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)); MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(IIOUtil.createStreamAdapter(pInput, mSize));
byte[] bom = new byte[2]; byte[] bom = new byte[2];
@ -67,8 +68,8 @@ final class PSDEXIF1Data extends PSDImageResource {
} }
// TIFF Image file directory (IFD) // TIFF Image file directory (IFD)
private static class Directory { static class Directory implements Iterable<Entry> {
List<Entry> mEntries = new ArrayList<Entry>(); private List<Entry> mEntries = new ArrayList<Entry>();
private Directory() {} private Directory() {}
@ -90,6 +91,20 @@ final class PSDEXIF1Data extends PSDImageResource {
return directory; return directory;
} }
public Entry get(int pTag) {
for (Entry entry : mEntries) {
if (entry.mTag == pTag) {
return entry;
}
}
return null;
}
public Iterator<Entry> iterator() {
return mEntries.iterator();
}
@Override @Override
public String toString() { public String toString() {
return String.format("Directory%s", mEntries); return String.format("Directory%s", mEntries);
@ -97,7 +112,7 @@ final class PSDEXIF1Data extends PSDImageResource {
} }
// TIFF IFD Entry // TIFF IFD Entry
private static class Entry { static class Entry {
private static final int EXIF_IFD = 0x8769; private static final int EXIF_IFD = 0x8769;
private final static String[] TYPE_NAMES = { private final static String[] TYPE_NAMES = {

View File

@ -0,0 +1,58 @@
package com.twelvemonkeys.imageio.plugins.psd;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
/**
* PSDGridAndGuideInfo
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDGridAndGuideInfo.java,v 1.0 Nov 7, 2009 8:46:13 PM haraldk Exp$
*/
final class PSDGridAndGuideInfo extends PSDImageResource {
/* Grid & guide header */
//typedef struct {
// guint32 fVersion; /* Version - always 1 for PS */
// guint32 fGridCycleV; /* Vertical grid size */
// guint32 fGridCycleH; /* Horizontal grid size */
// guint32 fGuideCount; /* Number of guides */
//} GuideHeader;
/* Guide resource block */
//typedef struct {
// guint32 fLocation; /* Guide position in Pixels * 100 */
// gchar fDirection; /* Guide orientation */
//} GuideResource;
int mVersion;
int mGridCycleVertical;
int mGridCycleHorizontal;
int mGuideCount;
GuideResource[] mGuides;
PSDGridAndGuideInfo(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
}
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
mVersion = pInput.readInt();
mGridCycleVertical = pInput.readInt();
mGridCycleHorizontal = pInput.readInt();
mGuideCount = pInput.readInt();
mGuides = new GuideResource[mGuideCount];
for (GuideResource guide : mGuides) {
guide.mLocation = pInput.readInt();
guide.mDirection = pInput.readByte();
}
}
static class GuideResource {
int mLocation;
byte mDirection; // 0: vertical, 1: horizontal
}
}

View File

@ -28,6 +28,9 @@
package com.twelvemonkeys.imageio.plugins.psd; package com.twelvemonkeys.imageio.plugins.psd;
import org.w3c.dom.Node;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import javax.imageio.IIOException; import javax.imageio.IIOException;
import java.io.IOException; import java.io.IOException;
@ -61,7 +64,7 @@ class PSDHeader {
final short mBits; final short mBits;
final short mMode; final short mMode;
PSDHeader(ImageInputStream pInput) throws IOException { PSDHeader(final ImageInputStream pInput) throws IOException {
int signature = pInput.readInt(); int signature = pInput.readInt();
if (signature != PSD.SIGNATURE_8BPS) { if (signature != PSD.SIGNATURE_8BPS) {
throw new IIOException("Not a PSD document, expected signature \"8BPS\": \"" + PSDUtil.intToStr(signature) + "\" (0x" + Integer.toHexString(signature) + ")"); throw new IIOException("Not a PSD document, expected signature \"8BPS\": \"" + PSDUtil.intToStr(signature) + "\" (0x" + Integer.toHexString(signature) + ")");

View File

@ -0,0 +1,418 @@
package com.twelvemonkeys.imageio.plugins.psd;
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.*;
import java.util.*;
/**
* PSDIPTCData
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDIPTCData.java,v 1.0 Nov 7, 2009 9:52:14 PM haraldk Exp$
*/
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;
PSDIPTCData(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
}
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
mDirectory = Directory.read(pInput, mSize);
}
@Override
public String toString() {
StringBuilder builder = toStringBuilder();
builder.append(", ").append(mDirectory);
builder.append("]");
return builder.toString();
}
static class Entry {
private int mTagId;
private String mValue;
public Entry(int pTagId, String pValue) {
mTagId = pTagId;
mValue = pValue;
}
@Override
public String toString() {
return (mTagId >> 8) + ":" + (mTagId & 0xff) + ": " + mValue;
}
}
static class Directory implements Iterable<Entry> {
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;
final List<Entry> mEntries = new ArrayList<Entry>();
private Directory() {}
@Override
public String toString() {
return "Directory" + mEntries.toString();
}
public Iterator<Entry> iterator() {
return mEntries.iterator();
}
public static Directory read(final ImageInputStream pInput, final long pSize) throws IOException {
Directory directory = new Directory();
final long streamEnd = pInput.getStreamPosition() + pSize;
// 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");
}
// 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 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);
}
return directory;
}
private void processTag(ImageInputStream pInput, int directoryType, int tagType, int tagByteCount) throws IOException {
int tagIdentifier = (directoryType << 8) | tagType;
String str = null;
switch (tagIdentifier) {
case IPTC.TAG_CODED_CHARACTER_SET:
// TODO: Use this encoding!?
// TODO: Move somewhere else?
mEncoding = parseEncoding(pInput, tagByteCount);
return;
case IPTC.TAG_RECORD_VERSION:
// short
str = Integer.toString(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:
// 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)";
}
else {
str = String.format("\"%s\"", parseString(pInput, tagByteCount));
}
}
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);
// }
}
// private Date getDateForTime(final Directory directory, final int tagIdentifier) {
// int dateTag;
//
// switch (tagIdentifier) {
// case IPTC.TAG_RELEASE_TIME:
// dateTag = IPTC.TAG_RELEASE_DATE;
// break;
// case IPTC.TAG_EXPIRATION_TIME:
// dateTag = IPTC.TAG_EXPIRATION_DATE;
// break;
// case IPTC.TAG_TIME_CREATED:
// dateTag = IPTC.TAG_DATE_CREATED;
// break;
// case IPTC.TAG_DIGITAL_CREATION_TIME:
// dateTag = IPTC.TAG_DIGITAL_CREATION_DATE;
// break;
// default:
// return new Date(0l);
// }
//
// return directory.containsTag(dateTag) ? directory.getDate(dateTag) : new Date(0l);
// }
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;
}
// private Date parseISO8601TimePart(final ImageInputStream pInputStream, int tagByteCount, final Date date) throws IOException {
// // ISO 8601: HHMMSS±HHMM
// if (tagByteCount >= 11) {
// String timeStr = parseString(pInputStream, tagByteCount);
// try {
// int hour = Integer.parseInt(timeStr.substring(0, 2));
// int minute = Integer.parseInt(timeStr.substring(2, 4));
// int second = Integer.parseInt(timeStr.substring(4, 6));
// String tzOffset = timeStr.substring(6, 11);
//
// TimeZone zone = new SimpleTimeZone(Integer.parseInt(tzOffset.charAt(0) == '+' ? tzOffset.substring(1) : tzOffset), tzOffset);
//
// GregorianCalendar calendar = new GregorianCalendar(zone);
// calendar.setTime(date);
//
// calendar.add(Calendar.HOUR_OF_DAY, hour);
// calendar.add(Calendar.MINUTE, minute);
// calendar.add(Calendar.SECOND, second);
//
// return calendar.getTime();
// }
// catch (NumberFormatException e) {
// // fall through and we'll store whatever was there as a String
// }
// }
// return null;
// }
//
// private Date parseISO8601DatePart(final ImageInputStream pInputStream, int tagByteCount) throws IOException {
// // ISO 8601: CCYYMMDD
// if (tagByteCount >= 8) {
// String dateStr = parseString(pInputStream, tagByteCount);
// try {
// int year = Integer.parseInt(dateStr.substring(0, 4));
// int month = Integer.parseInt(dateStr.substring(4, 6)) - 1;
// int day = Integer.parseInt(dateStr.substring(6, 8));
// GregorianCalendar calendar = new GregorianCalendar(year, month, day);
// return calendar.getTime();
// }
// catch (NumberFormatException e) {
// // fall through and we'll store whatever was there as a String
// }
// }
// return null;
// }
// 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.
// 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 {
// Will fail fast on illegal UTF-8-sequences
chars = decoder.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(data));
str = 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
str = StringUtil.decode(data, 0, data.length, "ISO8859_1");
}
return str;
}
}
static 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: Should we expose this field?
// /**
// * 2:199 JobMinder Assignment Data (Custom IPTC field).
// * A common custom IPTC field used by a now discontinued application called JobMinder.
// *
// * @see <a href="http://www.jobminder.net/">JobMinder Homepage</a>
// */
// static final int CUSTOM_TAG_JOBMINDER_ASSIGMENT_DATA = APPLICATION_RECORD | 199;
//
// // TODO: 2:150-2:154 Audio and 2:200-2:202 Object Preview Data
}
}

View File

@ -31,8 +31,15 @@ package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.image.ImageUtil; import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.imageio.ImageReaderBase; import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier; import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier;
import com.twelvemonkeys.xml.XMLSerializer;
import org.w3c.dom.Node;
import javax.imageio.*; import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.*; import java.awt.*;
@ -43,13 +50,11 @@ import java.awt.image.*;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List; import java.util.List;
/** /**
* ImageReader for Adobe Photoshop Document format. * ImageReader for Adobe Photoshop Document (PSD) format.
* *
* @see <a href="http://www.fileformat.info/format/psd/egff.htm">Adobe Photoshop File Format Summary<a> * @see <a href="http://www.fileformat.info/format/psd/egff.htm">Adobe Photoshop File Format Summary<a>
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a> * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
@ -57,18 +62,19 @@ import java.util.List;
* @version $Id: PSDImageReader.java,v 1.0 Apr 29, 2008 4:45:52 PM haraldk Exp$ * @version $Id: PSDImageReader.java,v 1.0 Apr 29, 2008 4:45:52 PM haraldk Exp$
*/ */
// TODO: Implement ImageIO meta data interface // TODO: Implement ImageIO meta data interface
// TODO: Allow reading separate (or some?) layers // TODO: API for reading separate layers
// TODO: Consider Romain Guy's Java 2D implementation of PS filters for the blending modes in layers // 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/ // http://www.curious-creature.org/2006/09/20/new-blendings-modes-for-java2d/
// See http://www.codeproject.com/KB/graphics/PSDParser.aspx // See http://www.codeproject.com/KB/graphics/PSDParser.aspx
// See http://www.adobeforums.com/webx?14@@.3bc381dc/0 // See http://www.adobeforums.com/webx?14@@.3bc381dc/0
public class PSDImageReader extends ImageReaderBase { public class PSDImageReader extends ImageReaderBase {
private PSDHeader mHeader; private PSDHeader mHeader;
private PSDColorData mColorData; // private PSDColorData mColorData;
private List<PSDImageResource> mImageResources; // private List<PSDImageResource> mImageResources;
private PSDGlobalLayerMask mGlobalLayerMask; // private PSDGlobalLayerMask mGlobalLayerMask;
private List<PSDLayerInfo> mLayerInfo; // private List<PSDLayerInfo> mLayerInfo;
private ICC_ColorSpace mColorSpace; private ICC_ColorSpace mColorSpace;
protected PSDMetadata mMetadata;
protected PSDImageReader(final ImageReaderSpi pOriginatingProvider) { protected PSDImageReader(final ImageReaderSpi pOriginatingProvider) {
super(pOriginatingProvider); super(pOriginatingProvider);
@ -76,8 +82,9 @@ public class PSDImageReader extends ImageReaderBase {
protected void resetMembers() { protected void resetMembers() {
mHeader = null; mHeader = null;
mColorData = null; // mColorData = null;
mImageResources = null; // mImageResources = null;
mMetadata = null;
mColorSpace = null; mColorSpace = null;
} }
@ -115,9 +122,9 @@ public class PSDImageReader extends ImageReaderBase {
); );
case PSD.COLOR_MODE_INDEXED: case PSD.COLOR_MODE_INDEXED:
// TODO: 16 bit indexed?! // TODO: 16 bit indexed?! Does it exist?
if (mHeader.mChannels == 1 && mHeader.mBits == 8) { if (mHeader.mChannels == 1 && mHeader.mBits == 8) {
return IndexedImageTypeSpecifier.createFromIndexColorModel(mColorData.getIndexColorModel()); return IndexedImageTypeSpecifier.createFromIndexColorModel(mMetadata.mColorData.getIndexColorModel());
} }
throw new IIOException( throw new IIOException(
@ -184,6 +191,11 @@ public class PSDImageReader extends ImageReaderBase {
throw new IIOException( throw new IIOException(
String.format("Unsupported channel count/bit depth for CMYK PSD: %d channels/%d bits", mHeader.mChannels, mHeader.mBits) String.format("Unsupported channel count/bit depth for CMYK PSD: %d channels/%d bits", mHeader.mChannels, mHeader.mBits)
); );
case PSD.COLOR_MODE_MULTICHANNEL:
// TODO: Implement
case PSD.COLOR_MODE_LAB:
// TODO: Implement
default: default:
throw new IIOException( throw new IIOException(
String.format("Unsupported PSD MODE: %s (%d channels/%d bits)", mHeader.mMode, mHeader.mChannels, mHeader.mBits) String.format("Unsupported PSD MODE: %s (%d channels/%d bits)", mHeader.mMode, mHeader.mChannels, mHeader.mBits)
@ -254,7 +266,7 @@ public class PSDImageReader extends ImageReaderBase {
if (mColorSpace == null) { if (mColorSpace == null) {
ICC_Profile profile = null; ICC_Profile profile = null;
for (PSDImageResource resource : mImageResources) { for (PSDImageResource resource : mMetadata.mImageResources) {
if (resource instanceof ICCProfile) { if (resource instanceof ICCProfile) {
profile = ((ICCProfile) resource).getProfile(); profile = ((ICCProfile) resource).getProfile();
break; break;
@ -323,6 +335,8 @@ public class PSDImageReader extends ImageReaderBase {
int[] byteCounts = null; int[] byteCounts = null;
int compression = mImageInput.readShort(); int compression = mImageInput.readShort();
// TODO: Need to make sure compression is set in metadata, even without reading the image data!
mMetadata.mCompression = compression;
switch (compression) { switch (compression) {
case PSD.COMPRESSION_NONE: case PSD.COMPRESSION_NONE:
@ -336,7 +350,7 @@ public class PSDImageReader extends ImageReaderBase {
break; break;
case PSD.COMPRESSION_ZIP: case PSD.COMPRESSION_ZIP:
// TODO: Could probably use the ZIPDecoder (DeflateDecoder) here.. // TODO: Could probably use the ZIPDecoder (DeflateDecoder) here..
case PSD.COMPRESSION_ZIP_PREDICTON: case PSD.COMPRESSION_ZIP_PREDICTION:
// TODO: Need to find out if the normal java.util.zip can handle this... // TODO: Need to find out if the normal java.util.zip can handle this...
// Could be same as PNG prediction? Read up... // Could be same as PNG prediction? Read up...
throw new IIOException("ZIP compression not supported yet"); throw new IIOException("ZIP compression not supported yet");
@ -404,7 +418,7 @@ public class PSDImageReader extends ImageReaderBase {
read16bitChannel(c, mHeader.mChannels, data16, interleavedBands, bandOffset, pSourceCM, row16, pSource, pDest, pXSub, pYSub, mHeader.mWidth, mHeader.mHeight, pByteCounts, c * mHeader.mHeight, pCompression == PSD.COMPRESSION_RLE); read16bitChannel(c, mHeader.mChannels, data16, interleavedBands, bandOffset, pSourceCM, row16, pSource, pDest, pXSub, pYSub, mHeader.mWidth, mHeader.mHeight, pByteCounts, c * mHeader.mHeight, pCompression == PSD.COMPRESSION_RLE);
break; break;
default: default:
throw new IIOException("Unknown PSD bit depth: " + mHeader.mBits); throw new IIOException(String.format("Unknown PSD bit depth: %s", mHeader.mBits));
} }
if (abortRequested()) { if (abortRequested()) {
@ -536,6 +550,7 @@ public class PSDImageReader extends ImageReaderBase {
} }
} }
@SuppressWarnings({"UnusedDeclaration"})
private void read1bitChannel(final int pChannel, final int pChannelCount, private void read1bitChannel(final int pChannel, final int pChannelCount,
final byte[] pData, final int pBands, final int pBandOffset, final byte[] pData, final int pBands, final int pBandOffset,
final ColorModel pSourceColorModel, final ColorModel pSourceColorModel,
@ -682,6 +697,9 @@ public class PSDImageReader extends ImageReaderBase {
if (mHeader == null) { if (mHeader == null) {
mHeader = new PSDHeader(mImageInput); mHeader = new PSDHeader(mImageInput);
mMetadata = new PSDMetadata();
mMetadata.mHeader = mHeader;
/* /*
Contains the required data to define the color mode. Contains the required data to define the color mode.
@ -694,9 +712,10 @@ public class PSDImageReader extends ImageReaderBase {
around as a black box for use when saving the file. around as a black box for use when saving the file.
*/ */
if (mHeader.mMode == PSD.COLOR_MODE_INDEXED) { if (mHeader.mMode == PSD.COLOR_MODE_INDEXED) {
mColorData = new PSDColorData(mImageInput); mMetadata.mColorData = new PSDColorData(mImageInput);
} }
else { else {
// TODO: We need to store the duotone spec if we decide to create a writer...
// Skip color mode data for other modes // Skip color mode data for other modes
long length = mImageInput.readUnsignedInt(); long length = mImageInput.readUnsignedInt();
mImageInput.skipBytes(length); mImageInput.skipBytes(length);
@ -708,6 +727,7 @@ public class PSDImageReader extends ImageReaderBase {
} }
// TODO: Flags or list of interesting resources to parse // TODO: Flags or list of interesting resources to parse
// TODO: Obey ignoreMetadata
private void readImageResources(final boolean pParseData) throws IOException { private void readImageResources(final boolean pParseData) throws IOException {
// TODO: Avoid unnecessary stream repositioning // TODO: Avoid unnecessary stream repositioning
long pos = mImageInput.getFlushedPosition(); long pos = mImageInput.getFlushedPosition();
@ -716,14 +736,14 @@ public class PSDImageReader extends ImageReaderBase {
long length = mImageInput.readUnsignedInt(); long length = mImageInput.readUnsignedInt();
if (pParseData && length > 0) { if (pParseData && length > 0) {
if (mImageResources == null) { if (mMetadata.mImageResources == null) {
mImageResources = new ArrayList<PSDImageResource>(); mMetadata.mImageResources = new ArrayList<PSDImageResource>();
long expectedEnd = mImageInput.getStreamPosition() + length; long expectedEnd = mImageInput.getStreamPosition() + length;
while (mImageInput.getStreamPosition() < expectedEnd) { while (mImageInput.getStreamPosition() < expectedEnd) {
// TODO: Have PSDImageResources defer actual parsing? (Just store stream offsets) // TODO: Have PSDImageResources defer actual parsing? (Just store stream offsets)
PSDImageResource resource = PSDImageResource.read(mImageInput); PSDImageResource resource = PSDImageResource.read(mImageInput);
mImageResources.add(resource); mMetadata.mImageResources.add(resource);
} }
if (mImageInput.getStreamPosition() != expectedEnd) { if (mImageInput.getStreamPosition() != expectedEnd) {
@ -735,6 +755,8 @@ public class PSDImageReader extends ImageReaderBase {
mImageInput.seek(pos + length + 4); mImageInput.seek(pos + length + 4);
} }
// TODO: Flags or list of interesting resources to parse
// TODO: Obey ignoreMetadata
private void readLayerAndMaskInfo(final boolean pParseData) throws IOException { private void readLayerAndMaskInfo(final boolean pParseData) throws IOException {
// TODO: Make sure we are positioned correctly // TODO: Make sure we are positioned correctly
long length = mImageInput.readUnsignedInt(); long length = mImageInput.readUnsignedInt();
@ -755,7 +777,7 @@ public class PSDImageReader extends ImageReaderBase {
for (int i = 0; i < layerInfos.length; i++) { for (int i = 0; i < layerInfos.length; i++) {
layerInfos[i] = new PSDLayerInfo(mImageInput); layerInfos[i] = new PSDLayerInfo(mImageInput);
} }
mLayerInfo = Arrays.asList(layerInfos); mMetadata.mLayerInfo = Arrays.asList(layerInfos);
// TODO: Clean-up // TODO: Clean-up
mImageInput.mark(); mImageInput.mark();
@ -767,10 +789,10 @@ public class PSDImageReader extends ImageReaderBase {
// TODO: If not explicitly needed, skip layers... // TODO: If not explicitly needed, skip layers...
BufferedImage layer = readLayerData(layerInfo, raw, imageType); BufferedImage layer = readLayerData(layerInfo, raw, imageType);
// TODO: Don't show! Store in metadata somehow... // TODO: Don't show! Store in meta data somehow...
if (layer != null) { // if (layer != null) {
showIt(layer, layerInfo.mLayerName + " " + layerInfo.mBlendMode.toString()); // showIt(layer, layerInfo.mLayerName + " " + layerInfo.mBlendMode.toString());
} // }
} }
long read = mImageInput.getStreamPosition() - pos; long read = mImageInput.getStreamPosition() - pos;
@ -784,7 +806,7 @@ public class PSDImageReader extends ImageReaderBase {
long layerMaskInfoLength = mImageInput.readUnsignedInt(); long layerMaskInfoLength = mImageInput.readUnsignedInt();
// System.out.println("GlobalLayerMaskInfo length: " + layerMaskInfoLength); // System.out.println("GlobalLayerMaskInfo length: " + layerMaskInfoLength);
if (layerMaskInfoLength > 0) { if (layerMaskInfoLength > 0) {
mGlobalLayerMask = new PSDGlobalLayerMask(mImageInput); mMetadata.mGlobalLayerMask = new PSDGlobalLayerMask(mImageInput);
// System.out.println("mGlobalLayerMask: " + mGlobalLayerMask); // System.out.println("mGlobalLayerMask: " + mGlobalLayerMask);
} }
@ -795,6 +817,7 @@ public class PSDImageReader extends ImageReaderBase {
mImageInput.skipBytes(toSkip); mImageInput.skipBytes(toSkip);
} }
else { else {
// Skip entire layer and mask section
mImageInput.skipBytes(length); mImageInput.skipBytes(length);
} }
} }
@ -838,7 +861,7 @@ public class PSDImageReader extends ImageReaderBase {
} }
else { else {
// 0 = red, 1 = green, etc // 0 = red, 1 = green, etc
// ?1 = transparency mask; ?2 = user supplied layer mask // -1 = transparency mask; -2 = user supplied layer mask
int c = channelInfo.mChannelId == -1 ? pLayerInfo.mChannelInfo.length - 1 : channelInfo.mChannelId; int c = channelInfo.mChannelId == -1 ? pLayerInfo.mChannelInfo.length - 1 : channelInfo.mChannelId;
// NOTE: For layers, byte counts are written per channel, while for the composite data // NOTE: For layers, byte counts are written per channel, while for the composite data
@ -861,7 +884,7 @@ public class PSDImageReader extends ImageReaderBase {
break; break;
case PSD.COMPRESSION_ZIP: case PSD.COMPRESSION_ZIP:
case PSD.COMPRESSION_ZIP_PREDICTON: case PSD.COMPRESSION_ZIP_PREDICTION:
default: default:
// Explicitly skipped above // Explicitly skipped above
throw new AssertionError(String.format("Unsupported layer data. Compression: %d", compression)); throw new AssertionError(String.format("Unsupported layer data. Compression: %d", compression));
@ -892,7 +915,7 @@ public class PSDImageReader extends ImageReaderBase {
read16bitChannel(c, imageType.getNumBands(), data16, interleavedBands, bandOffset, sourceCM, row16, area, area, xsub, ysub, width, height, byteCounts, 0, compression == PSD.COMPRESSION_RLE); read16bitChannel(c, imageType.getNumBands(), data16, interleavedBands, bandOffset, sourceCM, row16, area, area, xsub, ysub, width, height, byteCounts, 0, compression == PSD.COMPRESSION_RLE);
break; break;
default: default:
throw new IIOException("Unknown PSD bit depth: " + mHeader.mBits); throw new IIOException(String.format("Unknown PSD bit depth: %s", mHeader.mBits));
} }
if (abortRequested()) { if (abortRequested()) {
@ -931,6 +954,60 @@ public class PSDImageReader extends ImageReaderBase {
return pOriginal; return pOriginal;
} }
/// Layer support
// TODO: For now, leave as Metadata
/*
int getNumLayers(int pImageIndex) throws IOException;
boolean hasLayers(int pImageIndex) throws IOException;
BufferedImage readLayer(int pImageIndex, int pLayerIndex, ImageReadParam pParam) throws IOException;
int getLayerWidth(int pImageIndex, int pLayerIndex) throws IOException;
int getLayerHeight(int pImageIndex, int pLayerIndex) throws IOException;
// ?
Point getLayerOffset(int pImageIndex, int pLayerIndex) throws IOException;
*/
/// Metadata support
// TODO
@Override
public IIOMetadata getStreamMetadata() throws IOException {
// null might be appropriate here
// "For image formats that contain a single image, only image metadata is used."
return super.getStreamMetadata();
}
@Override
public IIOMetadata getImageMetadata(final int pImageIndex) throws IOException {
// TODO: Implement
checkBounds(pImageIndex);
readHeader();
readImageResources(true);
readLayerAndMaskInfo(true);
// TODO: Need to make sure compression is set in metadata, even without reading the image data!
mMetadata.mCompression = mImageInput.readShort();
// mMetadata.mHeader = mHeader;
// mMetadata.mColorData = mColorData;
// mMetadata.mImageResources = mImageResources;
return mMetadata; // TODO: clone if we change to mutable metadata
}
@Override
public IIOMetadata getImageMetadata(final int imageIndex, final String formatName, final Set<String> nodeNames) throws IOException {
// TODO: It might make sense to overload this, as there's loads of meta data in the file
return super.getImageMetadata(imageIndex, formatName, nodeNames);
}
/// Thumbnail support /// Thumbnail support
@Override @Override
public boolean readerSupportsThumbnails() { public boolean readerSupportsThumbnails() {
@ -944,14 +1021,14 @@ public class PSDImageReader extends ImageReaderBase {
List<PSDThumbnail> thumbnails = null; List<PSDThumbnail> thumbnails = null;
if (mImageResources == null) { if (mMetadata.mImageResources == null) {
// TODO: Need flag here, to specify what resources to read... // TODO: Need flag here, to specify what resources to read...
readImageResources(true); readImageResources(true);
// TODO: Skip this, requires storing some stream offsets // TODO: Skip this, requires storing some stream offsets
readLayerAndMaskInfo(false); readLayerAndMaskInfo(false);
} }
for (PSDImageResource resource : mImageResources) { for (PSDImageResource resource : mMetadata.mImageResources) {
if (resource instanceof PSDThumbnail) { if (resource instanceof PSDThumbnail) {
if (thumbnails == null) { if (thumbnails == null) {
thumbnails = new ArrayList<PSDThumbnail>(); thumbnails = new ArrayList<PSDThumbnail>();
@ -965,13 +1042,13 @@ public class PSDImageReader extends ImageReaderBase {
} }
@Override @Override
public int getNumThumbnails(int pIndex) throws IOException { public int getNumThumbnails(final int pIndex) throws IOException {
List<PSDThumbnail> thumbnails = getThumbnailResources(pIndex); List<PSDThumbnail> thumbnails = getThumbnailResources(pIndex);
return thumbnails == null ? 0 : thumbnails.size(); return thumbnails == null ? 0 : thumbnails.size();
} }
private PSDThumbnail getThumbnailResource(int pImageIndex, int pThumbnailIndex) throws IOException { private PSDThumbnail getThumbnailResource(final int pImageIndex, final int pThumbnailIndex) throws IOException {
List<PSDThumbnail> thumbnails = getThumbnailResources(pImageIndex); List<PSDThumbnail> thumbnails = getThumbnailResources(pImageIndex);
if (thumbnails == null) { if (thumbnails == null) {
@ -982,17 +1059,17 @@ public class PSDImageReader extends ImageReaderBase {
} }
@Override @Override
public int getThumbnailWidth(int pImageIndex, int pThumbnailIndex) throws IOException { public int getThumbnailWidth(final int pImageIndex, final int pThumbnailIndex) throws IOException {
return getThumbnailResource(pImageIndex, pThumbnailIndex).getWidth(); return getThumbnailResource(pImageIndex, pThumbnailIndex).getWidth();
} }
@Override @Override
public int getThumbnailHeight(int pImageIndex, int pThumbnailIndex) throws IOException { public int getThumbnailHeight(final int pImageIndex, final int pThumbnailIndex) throws IOException {
return getThumbnailResource(pImageIndex, pThumbnailIndex).getHeight(); return getThumbnailResource(pImageIndex, pThumbnailIndex).getHeight();
} }
@Override @Override
public BufferedImage readThumbnail(int pImageIndex, int pThumbnailIndex) throws IOException { public BufferedImage readThumbnail(final int pImageIndex, final int pThumbnailIndex) throws IOException {
// TODO: Thumbnail progress listeners... // TODO: Thumbnail progress listeners...
PSDThumbnail thumbnail = getThumbnailResource(pImageIndex, pThumbnailIndex); PSDThumbnail thumbnail = getThumbnailResource(pImageIndex, pThumbnailIndex);
@ -1001,6 +1078,7 @@ public class PSDImageReader extends ImageReaderBase {
processThumbnailStarted(pImageIndex, pThumbnailIndex); processThumbnailStarted(pImageIndex, pThumbnailIndex);
processThumbnailComplete(); processThumbnailComplete();
// TODO: Returning a cached mutable thumbnail is not really safe...
return thumbnail.getThumbnail(); return thumbnail.getThumbnail();
} }
@ -1052,11 +1130,26 @@ public class PSDImageReader extends ImageReaderBase {
// System.out.println("imageReader.mHeader: " + imageReader.mHeader); // System.out.println("imageReader.mHeader: " + imageReader.mHeader);
imageReader.readImageResources(true); imageReader.readImageResources(true);
// System.out.println("imageReader.mImageResources: " + imageReader.mImageResources); System.out.println("imageReader.mImageResources: " + imageReader.mMetadata.mImageResources);
System.out.println();
imageReader.readLayerAndMaskInfo(true); imageReader.readLayerAndMaskInfo(true);
System.out.println("imageReader.mLayerInfo: " + imageReader.mLayerInfo); System.out.println("imageReader.mLayerInfo: " + imageReader.mMetadata.mLayerInfo);
// System.out.println("imageReader.mGlobalLayerMask: " + imageReader.mGlobalLayerMask); // System.out.println("imageReader.mGlobalLayerMask: " + imageReader.mGlobalLayerMask);
System.out.println();
IIOMetadata metadata = imageReader.getImageMetadata(0);
Node node;
XMLSerializer serializer;
node = metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
serializer = new XMLSerializer(System.out, System.getProperty("file.encoding"));
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.serialize(node, true);
if (imageReader.hasThumbnails(0)) { if (imageReader.hasThumbnails(0)) {
int thumbnails = imageReader.getNumThumbnails(0); int thumbnails = imageReader.getNumThumbnails(0);

View File

@ -67,8 +67,12 @@ public class PSDImageReaderSpi extends ImageReaderSpi {
STANDARD_INPUT_TYPE, STANDARD_INPUT_TYPE,
// new String[]{"com.twelvemkonkeys.imageio.plugins.psd.PSDImageWriterSpi"}, // new String[]{"com.twelvemkonkeys.imageio.plugins.psd.PSDImageWriterSpi"},
null, null,
true, null, null, null, null, true, // supports standard stream metadata
true, null, null, null, null null, null, // native stream format name and class
null, null, // extra stream formats
true, // supports standard image metadata
PSDMetadata.NATIVE_METADATA_FORMAT_NAME, PSDMetadata.NATIVE_METADATA_FORMAT_CLASS_NAME,
null, null // extra image metadata formats
); );
} }

View File

@ -28,6 +28,8 @@
package com.twelvemonkeys.imageio.plugins.psd; package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.lang.StringUtil;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import javax.imageio.IIOException; import javax.imageio.IIOException;
import java.io.IOException; import java.io.IOException;
@ -41,6 +43,9 @@ import java.lang.reflect.Field;
* @version $Id: PSDImageResource.java,v 1.0 Apr 29, 2008 5:49:06 PM haraldk Exp$ * @version $Id: PSDImageResource.java,v 1.0 Apr 29, 2008 5:49:06 PM haraldk Exp$
*/ */
class PSDImageResource { class PSDImageResource {
// TODO: Refactor image resources to separate package
// TODO: Change constructor to store stream offset and length only (+ possibly the name), defer reading
final short mId; final short mId;
final String mName; final String mName;
final long mSize; final long mSize;
@ -50,9 +55,17 @@ class PSDImageResource {
mName = PSDUtil.readPascalString(pInput); mName = PSDUtil.readPascalString(pInput);
// Skip pad
int nameSize = mName.length() + 1;
if (nameSize % 2 != 0) {
pInput.readByte();
}
mSize = pInput.readUnsignedInt(); mSize = pInput.readUnsignedInt();
readData(pInput); readData(pInput);
// TODO: Sanity check reading here?
// Data is even-padded // Data is even-padded
if (mSize % 2 != 0) { if (mSize % 2 != 0) {
pInput.read(); pInput.read();
@ -84,7 +97,10 @@ class PSDImageResource {
protected StringBuilder toStringBuilder() { protected StringBuilder toStringBuilder() {
StringBuilder builder = new StringBuilder(getClass().getSimpleName()); StringBuilder builder = new StringBuilder(getClass().getSimpleName());
builder.append(resourceTypeForId(mId)); String fakeType = resourceTypeForId(mId);
if (fakeType != null) {
builder.append("(").append(fakeType).append(")");
}
builder.append("[ID: 0x"); builder.append("[ID: 0x");
builder.append(Integer.toHexString(mId)); builder.append(Integer.toHexString(mId));
@ -103,26 +119,32 @@ class PSDImageResource {
case PSD.RES_ALPHA_CHANNEL_INFO: case PSD.RES_ALPHA_CHANNEL_INFO:
case PSD.RES_DISPLAY_INFO: case PSD.RES_DISPLAY_INFO:
case PSD.RES_PRINT_FLAGS: case PSD.RES_PRINT_FLAGS:
case PSD.RES_IPTC_NAA:
case PSD.RES_GRID_AND_GUIDES_INFO:
case PSD.RES_THUMBNAIL_PS4: case PSD.RES_THUMBNAIL_PS4:
case PSD.RES_THUMBNAIL: case PSD.RES_THUMBNAIL:
case PSD.RES_ICC_PROFILE: case PSD.RES_ICC_PROFILE:
case PSD.RES_VERSION_INFO:
case PSD.RES_EXIF_DATA_1: case PSD.RES_EXIF_DATA_1:
// case PSD.RES_EXIF_DATA_3: // case PSD.RES_EXIF_DATA_3:
case PSD.RES_XMP_DATA: case PSD.RES_XMP_DATA:
case PSD.RES_PRINT_SCALE:
case PSD.RES_PIXEL_ASPECT_RATIO:
case PSD.RES_PRINT_FLAGS_INFORMATION: case PSD.RES_PRINT_FLAGS_INFORMATION:
return ""; return null;
default: default:
try { try {
for (Field field : PSD.class.getDeclaredFields()) { for (Field field : PSD.class.getDeclaredFields()) {
if (field.getName().startsWith("RES_") && field.getInt(null) == pId) { if (field.getName().startsWith("RES_") && field.getInt(null) == pId) {
return "(" + field.getName().substring(4) + ")"; String name = field.getName().substring(4);
return StringUtil.lispToCamel(name.replace("_", "-").toLowerCase(), true);
} }
} }
} }
catch (IllegalAccessException ignore) { catch (IllegalAccessException ignore) {
} }
return "(unknown resource)"; return "UnknownResource";
} }
} }
@ -144,15 +166,27 @@ class PSDImageResource {
return new PSDDisplayInfo(id, pInput); return new PSDDisplayInfo(id, pInput);
case PSD.RES_PRINT_FLAGS: case PSD.RES_PRINT_FLAGS:
return new PSDPrintFlags(id, pInput); return new PSDPrintFlags(id, pInput);
case PSD.RES_IPTC_NAA:
return new PSDIPTCData(id, pInput);
case PSD.RES_GRID_AND_GUIDES_INFO:
return new PSDGridAndGuideInfo(id, pInput);
case PSD.RES_THUMBNAIL_PS4: case PSD.RES_THUMBNAIL_PS4:
case PSD.RES_THUMBNAIL: case PSD.RES_THUMBNAIL:
return new PSDThumbnail(id, pInput); return new PSDThumbnail(id, pInput);
case PSD.RES_ICC_PROFILE: case PSD.RES_ICC_PROFILE:
return new ICCProfile(id, pInput); return new ICCProfile(id, pInput);
case PSD.RES_UNICODE_ALPHA_NAMES:
return new PSDUnicodeAlphaNames(id, pInput);
case PSD.RES_VERSION_INFO:
return new PSDVersionInfo(id, pInput);
case PSD.RES_EXIF_DATA_1: case PSD.RES_EXIF_DATA_1:
return new PSDEXIF1Data(id, pInput); return new PSDEXIF1Data(id, pInput);
case PSD.RES_XMP_DATA: case PSD.RES_XMP_DATA:
return new PSDXMPData(id, pInput); return new PSDXMPData(id, pInput);
case PSD.RES_PRINT_SCALE:
return new PSDPrintScale(id, pInput);
case PSD.RES_PIXEL_ASPECT_RATIO:
return new PSDPixelAspectRatio(id, pInput);
case PSD.RES_PRINT_FLAGS_INFORMATION: case PSD.RES_PRINT_FLAGS_INFORMATION:
return new PSDPrintFlagsInformation(id, pInput); return new PSDPrintFlagsInformation(id, pInput);
default: default:

View File

@ -98,14 +98,12 @@ class PSDLayerInfo {
mLayerName = PSDUtil.readPascalString(pInput); mLayerName = PSDUtil.readPascalString(pInput);
int layerNameSize = mLayerName.length() + 1; int layerNameSize = mLayerName.length() + 1;
// readPascalString has already read pad byte for word alignment
if (layerNameSize % 2 != 0) { // Skip pad bytes for long word alignment
layerNameSize++;
}
// Skip two more pad bytes if needed
if (layerNameSize % 4 != 0) { if (layerNameSize % 4 != 0) {
pInput.skipBytes(2); int skip = layerNameSize % 4;
layerNameSize += 2; pInput.skipBytes(skip);
layerNameSize += skip;
} }
// TODO: There's some data skipped here... // TODO: There's some data skipped here...

View File

@ -0,0 +1,734 @@
package com.twelvemonkeys.imageio.plugins.psd;
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;
import java.awt.image.IndexColorModel;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
/**
* PSDMetadata
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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 {
// TODO: Decide on image/stream metadata...
static final String NATIVE_METADATA_FORMAT_NAME = "com_twelvemonkeys_imageio_psd_image_1.0";
static final String NATIVE_METADATA_FORMAT_CLASS_NAME = "com.twelvemonkeys.imageio.plugins.psd.PSDMetadataFormat";
PSDHeader mHeader;
PSDColorData mColorData;
int mCompression = -1;
List<PSDImageResource> mImageResources;
PSDGlobalLayerMask mGlobalLayerMask;
List<PSDLayerInfo> mLayerInfo;
static final String[] COLOR_MODES = {
"MONOCHROME", "GRAYSCALE", "INDEXED", "RGB", "CMYK", null, null, "MULTICHANNEL", "DUOTONE", "LAB"
};
static final String[] DISPLAY_INFO_CS = {
"RGB", "HSB", "CMYK", "PANTONE", "FOCOLTONE", "TRUMATCH", "TOYO", "LAB", "GRAYSCALE", null, "HKS", "DIC",
null, // TODO: ... (until index 2999),
"ANPA"
};
static final String[] DISPLAY_INFO_KINDS = {"selected", "protected"};
static final String[] RESOLUTION_UNITS = {null, "pixels/inch", "pixels/cm"};
static final String[] DIMENSION_UNITS = {null, "in", "cm", "pt", "picas", "columns"};
static final String[] JAVA_CS = {
"XYZ", "Lab", "Yuv", "YCbCr", "Yxy", "RGB", "GRAY", "HSV", "HLS", "CMYK", "CMY",
"2CLR", "3CLR", "4CLR", "5CLR", "6CLR", "7CLR", "8CLR", "9CLR", "ACLR", "BCLR", "CCLR", "DCLR", "ECLR", "FCLR"
};
static final String[] GUIDE_ORIENTATIONS = {"vertical", "horizontal"};
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() {
IIOMetadataNode root = new IIOMetadataNode(NATIVE_METADATA_FORMAT_NAME);
root.appendChild(createHeaderNode());
if (mHeader.mMode == PSD.COLOR_MODE_INDEXED) {
root.appendChild(createPaletteNode());
}
if (mImageResources != null && !mImageResources.isEmpty()) {
root.appendChild(createImageResourcesNode());
}
return root;
}
private Node createHeaderNode() {
IIOMetadataNode header = new IIOMetadataNode("Header");
header.setAttribute("type", "PSD");
header.setAttribute("version", "1");
header.setAttribute("channels", Integer.toString(mHeader.mChannels));
header.setAttribute("height", Integer.toString(mHeader.mHeight));
header.setAttribute("width", Integer.toString(mHeader.mWidth));
header.setAttribute("bits", Integer.toString(mHeader.mBits));
header.setAttribute("mode", COLOR_MODES[mHeader.mMode]);
return header;
}
private Node createImageResourcesNode() {
IIOMetadataNode resource = new IIOMetadataNode("ImageResources");
IIOMetadataNode node;
for (PSDImageResource imageResource : mImageResources) {
// TODO: Always add name (if set) and id (as resourceId) to all nodes?
// Resource Id is useful for people with access to the PSD spec..
if (imageResource instanceof ICCProfile) {
ICCProfile profile = (ICCProfile) imageResource;
// TODO: Format spec
node = new IIOMetadataNode("ICCProfile");
node.setAttribute("colorSpaceType", JAVA_CS[profile.getProfile().getColorSpaceType()]);
node.setUserObject(profile.getProfile());
}
else if (imageResource instanceof PSDAlphaChannelInfo) {
PSDAlphaChannelInfo alphaChannelInfo = (PSDAlphaChannelInfo) imageResource;
node = new IIOMetadataNode("AlphaChannelInfo");
for (String name : alphaChannelInfo.mNames) {
IIOMetadataNode nameNode = new IIOMetadataNode("Name");
nameNode.setAttribute("value", name);
node.appendChild(nameNode);
}
}
else if (imageResource instanceof PSDDisplayInfo) {
PSDDisplayInfo displayInfo = (PSDDisplayInfo) imageResource;
node = new IIOMetadataNode("DisplayInfo");
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));
}
node.setAttribute("colors", builder.toString());
node.setAttribute("opacity", Integer.toString(displayInfo.mOpacity));
node.setAttribute("kind", DISPLAY_INFO_KINDS[displayInfo.mKind]);
}
else if (imageResource instanceof PSDGridAndGuideInfo) {
PSDGridAndGuideInfo info = (PSDGridAndGuideInfo) imageResource;
node = new IIOMetadataNode("GridAndGuideInfo");
node.setAttribute("version", String.valueOf(info.mVersion));
node.setAttribute("verticalGridCycle", String.valueOf(info.mGridCycleVertical));
node.setAttribute("horizontalGridCycle", String.valueOf(info.mGridCycleHorizontal));
for (PSDGridAndGuideInfo.GuideResource guide : info.mGuides) {
IIOMetadataNode guideNode = new IIOMetadataNode("Guide");
guideNode.setAttribute("location", Integer.toString(guide.mLocation));
guideNode.setAttribute("orientation", GUIDE_ORIENTATIONS[guide.mDirection]);
}
}
else if (imageResource instanceof PSDPixelAspectRatio) {
PSDPixelAspectRatio aspectRatio = (PSDPixelAspectRatio) imageResource;
node = new IIOMetadataNode("PixelAspectRatio");
node.setAttribute("version", String.valueOf(aspectRatio.mVersion));
node.setAttribute("aspectRatio", String.valueOf(aspectRatio.mAspect));
}
else if (imageResource instanceof PSDPrintFlags) {
PSDPrintFlags flags = (PSDPrintFlags) imageResource;
node = new IIOMetadataNode("PrintFlags");
node.setAttribute("labels", String.valueOf(flags.mLabels));
node.setAttribute("cropMarks", String.valueOf(flags.mCropMasks));
node.setAttribute("colorBars", String.valueOf(flags.mColorBars));
node.setAttribute("registrationMarks", String.valueOf(flags.mRegistrationMarks));
node.setAttribute("negative", String.valueOf(flags.mNegative));
node.setAttribute("flip", String.valueOf(flags.mFlip));
node.setAttribute("interpolate", String.valueOf(flags.mInterpolate));
node.setAttribute("caption", String.valueOf(flags.mCaption));
}
else if (imageResource instanceof PSDPrintFlagsInformation) {
PSDPrintFlagsInformation information = (PSDPrintFlagsInformation) imageResource;
node = new IIOMetadataNode("PrintFlagsInformation");
node.setAttribute("version", String.valueOf(information.mVersion));
node.setAttribute("cropMarks", String.valueOf(information.mCropMasks));
node.setAttribute("field", String.valueOf(information.mField));
node.setAttribute("bleedWidth", String.valueOf(information.mBleedWidth));
node.setAttribute("bleedScale", String.valueOf(information.mBleedScale));
}
else if (imageResource instanceof PSDPrintScale) {
PSDPrintScale printScale = (PSDPrintScale) imageResource;
node = new IIOMetadataNode("PrintScale");
node.setAttribute("style", PRINT_SCALE_STYLES[printScale.mStyle]);
node.setAttribute("xLocation", String.valueOf(printScale.mXLocation));
node.setAttribute("yLocation", String.valueOf(printScale.mYlocation));
node.setAttribute("scale", String.valueOf(printScale.mScale));
}
else if (imageResource instanceof PSDResolutionInfo) {
PSDResolutionInfo information = (PSDResolutionInfo) imageResource;
node = new IIOMetadataNode("ResolutionInfo");
node.setAttribute("horizontalResolution", String.valueOf(information.mHRes));
node.setAttribute("horizontalResolutionUnit", RESOLUTION_UNITS[information.mHResUnit]);
node.setAttribute("widthUnit", DIMENSION_UNITS[information.mWidthUnit]);
node.setAttribute("verticalResolution", String.valueOf(information.mVRes));
node.setAttribute("verticalResolutionUnit", RESOLUTION_UNITS[information.mVResUnit]);
node.setAttribute("heightUnit", DIMENSION_UNITS[information.mHeightUnit]);
}
else if (imageResource instanceof PSDUnicodeAlphaNames) {
PSDUnicodeAlphaNames alphaNames = (PSDUnicodeAlphaNames) imageResource;
node = new IIOMetadataNode("UnicodeAlphaNames");
for (String name : alphaNames.mNames) {
IIOMetadataNode nameNode = new IIOMetadataNode("Name");
nameNode.setAttribute("value", name);
node.appendChild(nameNode);
}
}
else if (imageResource instanceof PSDVersionInfo) {
PSDVersionInfo information = (PSDVersionInfo) imageResource;
node = new IIOMetadataNode("VersionInfo");
node.setAttribute("version", String.valueOf(information.mVersion));
node.setAttribute("hasRealMergedData", String.valueOf(information.mHasRealMergedData));
node.setAttribute("writer", information.mWriter);
node.setAttribute("reader", information.mReader);
node.setAttribute("fileVersion", String.valueOf(information.mFileVersion));
}
else if (imageResource instanceof PSDThumbnail) {
// TODO: Revise/rethink this...
PSDThumbnail thumbnail = (PSDThumbnail) imageResource;
node = new IIOMetadataNode("Thumbnail");
// TODO: Thumbnail attributes + access to data, to avoid JPEG re-compression problems
node.setUserObject(thumbnail.getThumbnail());
}
else if (imageResource instanceof PSDIPTCData) {
// TODO: Revise/rethink this...
// Transcode to XMP? ;-)
PSDIPTCData iptc = (PSDIPTCData) imageResource;
node = new IIOMetadataNode("IPTC");
node.setUserObject(iptc.mDirectory);
}
else if (imageResource instanceof PSDEXIF1Data) {
// TODO: Revise/rethink this...
// Transcode to XMP? ;-)
PSDEXIF1Data exif = (PSDEXIF1Data) imageResource;
node = new IIOMetadataNode("EXIF");
node.setUserObject(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...
PSDXMPData xmp = (PSDXMPData) imageResource;
node = new IIOMetadataNode("XMP");
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(new InputSource(xmp.getData()));
// Set the entire XMP document as user data
node.setUserObject(document);
}
catch (Exception e) {
e.printStackTrace();
}
}
else {
// Generic resource..
node = new IIOMetadataNode(PSDImageResource.resourceTypeForId(imageResource.mId));
}
// TODO: More resources
node.setAttribute("resourceId", String.format("0x%04x", imageResource.mId));
resource.appendChild(node);
}
return resource;
}
/// Standard format support
@Override
protected IIOMetadataNode getStandardChromaNode() {
IIOMetadataNode chroma_node = new IIOMetadataNode("Chroma");
IIOMetadataNode node; // scratch node
node = new IIOMetadataNode("ColorSpaceType");
String cs;
switch (mHeader.mMode) {
case PSD.COLOR_MODE_MONOCHROME:
case PSD.COLOR_MODE_GRAYSCALE:
case PSD.COLOR_MODE_DUOTONE: // Rationale: Spec says treat as gray...
cs = "GRAY";
break;
case PSD.COLOR_MODE_RGB:
case PSD.COLOR_MODE_INDEXED:
cs = "RGB";
break;
case PSD.COLOR_MODE_CMYK:
cs = "CMYK";
break;
case PSD.COLOR_MODE_MULTICHANNEL:
cs = getMultiChannelCS(mHeader.mChannels);
break;
case PSD.COLOR_MODE_LAB:
cs = "Lab";
break;
default:
throw new AssertionError("Unreachable");
}
node.setAttribute("name", cs);
chroma_node.appendChild(node);
// TODO: Channels might be 5 for RGB + A + Mask... Probably not correct
node = new IIOMetadataNode("NumChannels");
node.setAttribute("value", Integer.toString(mHeader.mChannels));
chroma_node.appendChild(node);
// TODO: Check if this is correct with bitmap (monchrome)
node = new IIOMetadataNode("BlackIsZero");
node.setAttribute("value", "true");
chroma_node.appendChild(node);
if (mHeader.mMode == PSD.COLOR_MODE_INDEXED) {
node = createPaletteNode();
chroma_node.appendChild(node);
}
// TODO: Hardcode background color to white?
// if (bKGD_present) {
// if (bKGD_colorType == PNGImageReader.PNG_COLOR_PALETTE) {
// node = new IIOMetadataNode("BackgroundIndex");
// node.setAttribute("value", Integer.toString(bKGD_index));
// } else {
// node = new IIOMetadataNode("BackgroundColor");
// int r, g, b;
//
// if (bKGD_colorType == PNGImageReader.PNG_COLOR_GRAY) {
// r = g = b = bKGD_gray;
// } else {
// r = bKGD_red;
// g = bKGD_green;
// b = bKGD_blue;
// }
// node.setAttribute("red", Integer.toString(r));
// node.setAttribute("green", Integer.toString(g));
// node.setAttribute("blue", Integer.toString(b));
// }
// chroma_node.appendChild(node);
// }
return chroma_node;
}
private IIOMetadataNode createPaletteNode() {
IIOMetadataNode node = new IIOMetadataNode("Palette");
IndexColorModel cm = mColorData.getIndexColorModel();
for (int i = 0; i < cm.getMapSize(); i++) {
IIOMetadataNode entry = new IIOMetadataNode("PaletteEntry");
entry.setAttribute("index", Integer.toString(i));
entry.setAttribute("red", Integer.toString(cm.getRed(i)));
entry.setAttribute("green", Integer.toString(cm.getGreen(i)));
entry.setAttribute("blue", Integer.toString(cm.getBlue(i)));
node.appendChild(entry);
}
return node;
}
private String getMultiChannelCS(short pChannels) {
if (pChannels < 16) {
return Integer.toHexString(pChannels) + "CLR";
}
throw new UnsupportedOperationException("Standard meta data format does not support more than 15 channels");
}
@Override
protected IIOMetadataNode getStandardCompressionNode() {
IIOMetadataNode compression_node = 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";
break;
case PSD.COMPRESSION_ZIP:
case PSD.COMPRESSION_ZIP_PREDICTION:
compression = "zip";
break;
default:
throw new AssertionError("Unreachable");
}
node.setAttribute("value", compression);
compression_node.appendChild(node);
node = new IIOMetadataNode("Lossless");
node.setAttribute("value", "true");
compression_node.appendChild(node);
return compression_node;
}
@Override
protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode data_node = new IIOMetadataNode("Data");
IIOMetadataNode node; // scratch node
node = new IIOMetadataNode("PlanarConfiguration");
node.setAttribute("value", "PlaneInterleaved"); // TODO: Check with spec
data_node.appendChild(node);
node = new IIOMetadataNode("SampleFormat");
node.setAttribute("value", mHeader.mMode == PSD.COLOR_MODE_INDEXED ? "Index" : "UnsignedIntegral");
data_node.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);
// TODO: SampleMSB? Or is network (aka Motorola/big endian) byte order assumed?
return data_node;
}
@Override
protected IIOMetadataNode getStandardDimensionNode() {
IIOMetadataNode dimension_node = 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);
node = new IIOMetadataNode("ImageOrientation");
node.setAttribute("value", "Normal");
dimension_node.appendChild(node);
Iterator<PSDResolutionInfo> 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);
node = new IIOMetadataNode("VerticalPixelSize");
node.setAttribute("value", Float.toString(asMM(resolutionInfo.mVResUnit, resolutionInfo.mVRes)));
dimension_node.appendChild(node);
}
// TODO:
/*
<!ELEMENT "HorizontalPixelOffset" EMPTY>
<!-- The horizonal position, in pixels, where the image should be
rendered onto a raster display -->
<!ATTLIST "HorizontalPixelOffset" "value" #CDATA #REQUIRED>
<!-- Data type: Integer -->
<!ELEMENT "VerticalPixelOffset" EMPTY>
<!-- The vertical position, in pixels, where the image should be
rendered onto a raster display -->
<!ATTLIST "VerticalPixelOffset" "value" #CDATA #REQUIRED>
<!-- Data type: Integer -->
<!ELEMENT "HorizontalScreenSize" EMPTY>
<!-- The width, in pixels, of the raster display into which the
image should be rendered -->
<!ATTLIST "HorizontalScreenSize" "value" #CDATA #REQUIRED>
<!-- Data type: Integer -->
<!ELEMENT "VerticalScreenSize" EMPTY>
<!-- The height, in pixels, of the raster display into which the
image should be rendered -->
<!ATTLIST "VerticalScreenSize" "value" #CDATA #REQUIRED>
<!-- Data type: Integer -->
*/
return dimension_node;
}
private static float asMM(final short pUnit, final float pResolution) {
// Unit: 1 -> pixels per inch, 2 -> pixels pr cm
return (pUnit == 1 ? 25.4f : 10) / pResolution;
}
@Override
protected IIOMetadataNode getStandardDocumentNode() {
IIOMetadataNode document_node = new IIOMetadataNode("Document");
IIOMetadataNode node; // scratch node
node = new IIOMetadataNode("FormatVersion");
node.setAttribute("value", "1"); // PSD format version is always 1
document_node.appendChild(node);
// Get EXIF data if present
Iterator<PSDEXIF1Data> exif = getResources(PSDEXIF1Data.class);
if (exif.hasNext()) {
PSDEXIF1Data data = exif.next();
// Get the EXIF DateTime (aka ModifyDate) tag if present
PSDEXIF1Data.Entry dateTime = data.mDirectory.get(0x0132); // TODO: Constant
if (dateTime != null) {
node = new IIOMetadataNode("ImageModificationTime");
// Format: "YYYY:MM:DD hh:mm:ss" (with quotes! :-P)
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));
document_node.appendChild(node);
}
}
return document_node;
}
@Override
protected IIOMetadataNode getStandardTextNode() {
// TODO: CaptionDigest?, EXIF, XMP
Iterator<PSDImageResource> textResources = getResources(PSDEXIF1Data.class, PSDXMPData.class);
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;
}
@Override
protected IIOMetadataNode getStandardTileNode() {
return super.getStandardTileNode();
}
@Override
protected IIOMetadataNode getStandardTransparencyNode() {
IIOMetadataNode transparency_node = new IIOMetadataNode("Transparency");
IIOMetadataNode node; // scratch node
node = new IIOMetadataNode("Alpha");
node.setAttribute("value", hasAlpha() ? "nonpremultipled" : "none"); // TODO: Check spec
transparency_node.appendChild(node);
return transparency_node;
}
private boolean hasAlpha() {
return mHeader.mMode == PSD.COLOR_MODE_RGB && mHeader.mChannels >= 4 ||
mHeader.mMode == PSD.COLOR_MODE_CMYK & mHeader.mChannels >= 5;
}
<T extends PSDImageResource> Iterator<T> getResources(final Class<T> pResourceType) {
// NOTE: The cast here is wrong, strictly speaking, but it does not matter...
@SuppressWarnings({"unchecked"})
Iterator<T> iterator = (Iterator<T>) mImageResources.iterator();
return new FilterIterator<T>(iterator, new FilterIterator.Filter<T>() {
public boolean accept(final T pElement) {
return pResourceType.isInstance(pElement);
}
});
}
Iterator<PSDImageResource> getResources(final Class<? extends PSDImageResource>... 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)) {
return true;
}
}
return false;
}
});
}
}

View File

@ -0,0 +1,212 @@
package com.twelvemonkeys.imageio.plugins.psd;
import org.w3c.dom.Document;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import java.awt.image.BufferedImage;
import java.util.Arrays;
/**
* PSDMetadataFormat
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDMetadataFormat.java,v 1.0 Nov 4, 2009 5:27:53 PM haraldk Exp$
*/
public final class PSDMetadataFormat extends IIOMetadataFormatImpl {
private final static PSDMetadataFormat sInstance = new PSDMetadataFormat();
/**
* Private constructor.
* <p/>
* The {@link javax.imageio.metadata.IIOMetadata} class will instantiate this class
* by reflection, invoking the static {@code getInstance()} method.
*
* @see javax.imageio.metadata.IIOMetadata#getMetadataFormat
* @see #getInstance()
*/
private PSDMetadataFormat() {
// Defines the root element
super(PSDMetadata.NATIVE_METADATA_FORMAT_NAME, CHILD_POLICY_SOME);
// root -> PSDHeader
// TODO: How do I specify that the header is required?
addElement("Header", PSDMetadata.NATIVE_METADATA_FORMAT_NAME, CHILD_POLICY_EMPTY);
addAttribute("Header", "type", DATATYPE_STRING, false, "PSD", Arrays.asList("PSD", "PSB"));
addAttribute("Header", "version", DATATYPE_INTEGER, false, "1", Arrays.asList("1"));
addAttribute("Header", "channels", DATATYPE_INTEGER, true, null, "1", "24", true, true);
// rows?
addAttribute("Header", "height", DATATYPE_INTEGER, true, null, "1", "30000", true, true);
// 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));
/*
Contains the required data to define the color mode.
For indexed color images, the count will be equal to 768, and the mode data
will contain the color table for the image, in non-interleaved order.
For duotone images, the mode data will contain the duotone specification,
the format of which is not documented. Non-Photoshop readers can treat
the duotone image as a grayscale image, and keep the duotone specification
around as a black box for use when saving the file.
*/
// root -> Palette
// Color map for indexed, optional
// NOTE: Palette, PaletteEntry naming taken from the standard format, native PSD naming is ColorModeData
// NOTE: PSD stores these as 256 Red, 256 Green, 256 Blue.. Should we do the same in the meta data?
addElement("Palette", PSDMetadata.NATIVE_METADATA_FORMAT_NAME, 256, 256); // 768 = 256 * 3
addElement("PaletteEntry", "Palette", CHILD_POLICY_EMPTY);
addAttribute("PaletteEntry", "index", DATATYPE_INTEGER, true, null, "0", "255", true, true);
addAttribute("PaletteEntry", "red", DATATYPE_INTEGER, true, null, "0", "255", true, true);
addAttribute("PaletteEntry", "green", DATATYPE_INTEGER, true, null, "0", "255", true, true);
addAttribute("PaletteEntry", "blue", DATATYPE_INTEGER, true, null, "0", "255", true, true);
// No alpha allowed in indexed color PSD
// TODO: Duotone spec, optional (use same element as palette?)
// Or use object or raw bytes..
// root -> ImageResources
// Image resources, optional
addElement("ImageResources", PSDMetadata.NATIVE_METADATA_FORMAT_NAME, CHILD_POLICY_SEQUENCE); // SOME?
// root -> ImageResources -> ImageResource
// Generic resource
addElement("ImageResource", "ImageResources", CHILD_POLICY_ALL);
// TODO: Allow arbitrary values to be added as a generic resource...
// root -> ImageResources -> AlphaChannelInfo
addElement("AlphaChannelInfo", "ImageResources", 0, Integer.MAX_VALUE); // The format probably does not support that many layers..
addElement("Name", "AlphaChannelInfo", CHILD_POLICY_EMPTY);
addAttribute("Name", "value", DATATYPE_STRING, true, null);
// root -> ImageResources -> DisplayInfo
addElement("DisplayInfo", "ImageResources", CHILD_POLICY_EMPTY);
// TODO: Consider using human readable strings
// TODO: Limit values (0-8, 10, 11, 3000)
addAttribute("DisplayInfo", "colorSpace", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.DISPLAY_INFO_CS));
addAttribute("DisplayInfo", "colors", DATATYPE_INTEGER, true, 4, 4);
addAttribute("DisplayInfo", "opacity", DATATYPE_INTEGER, true, null, "0", "100", true, true);
// TODO: Consider using human readable strings
addAttribute("DisplayInfo", "kind", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.DISPLAY_INFO_KINDS));
// root -> ImageResources -> EXIF
addElement("EXIF", "ImageResources", CHILD_POLICY_EMPTY);
addObjectValue("EXIF", PSDEXIF1Data.Directory.class, true, null);
// TODO: Incorporate EXIF / TIFF metadata here somehow... (or treat as opaque bytes?)
// root -> ImageResources -> GridAndGuideInfo
addElement("GridAndGuideInfo", "ImageResources", 0, Integer.MAX_VALUE);
addAttribute("GridAndGuideInfo", "version", DATATYPE_INTEGER, false, "1");
addAttribute("GridAndGuideInfo", "verticalGridCycle", DATATYPE_INTEGER, false, "576");
addAttribute("GridAndGuideInfo", "horizontalGridCycle", DATATYPE_INTEGER, false, "576");
addElement("Guide", "GridAndGuideInfo", CHILD_POLICY_EMPTY);
addAttribute("Guide", "location", DATATYPE_INTEGER, true, null, "0", Integer.toString(Integer.MAX_VALUE), true, true);
addAttribute("Guide", "orientation", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.GUIDE_ORIENTATIONS));
// root -> ImageResources -> ICCProfile
addElement("ICCProfile", "ImageResources", CHILD_POLICY_EMPTY);
addAttribute("ICCProfile", "colorSpaceType", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.JAVA_CS));
// root -> ImageResources -> IPTC
addElement("IPTC", "ImageResources", CHILD_POLICY_EMPTY);
addObjectValue("IPTC", PSDIPTCData.Directory.class, true, null);
// TODO: Incorporate IPTC metadata here somehow... (or treat as opaque bytes?)
// root -> ImageResources -> PixelAspectRatio
addElement("PixelAspectRatio", "ImageResources", CHILD_POLICY_EMPTY);
addAttribute("PixelAspectRatio", "version", DATATYPE_STRING, false, "1");
addAttribute("PixelAspectRatio", "aspectRatio", DATATYPE_DOUBLE, true, null, "0", Double.toString(Double.POSITIVE_INFINITY), true, false);
// root -> ImageResources -> PrintFlags
addElement("PrintFlags", "ImageResources", CHILD_POLICY_EMPTY);
addBooleanAttribute("PrintFlags", "labels", false, false);
addBooleanAttribute("PrintFlags", "cropMasks", false, false);
addBooleanAttribute("PrintFlags", "colorBars", false, false);
addBooleanAttribute("PrintFlags", "registrationMarks", false, false);
addBooleanAttribute("PrintFlags", "negative", false, false);
addBooleanAttribute("PrintFlags", "flip", false, false);
addBooleanAttribute("PrintFlags", "interpolate", false, false);
addBooleanAttribute("PrintFlags", "caption", false, false);
// root -> ImageResources -> PrintFlagsInformation
addElement("PrintFlagsInformation", "ImageResources", CHILD_POLICY_EMPTY);
addAttribute("PrintFlagsInformation", "version", DATATYPE_INTEGER, false, "1");
addBooleanAttribute("PrintFlagsInformation", "cropMarks", false, false);
addAttribute("PrintFlagsInformation", "field", DATATYPE_INTEGER, true, "0");
addAttribute("PrintFlagsInformation", "bleedWidth", DATATYPE_INTEGER, true, null, "0", String.valueOf(Long.MAX_VALUE), true, true); // TODO: LONG??!
addAttribute("PrintFlagsInformation", "bleedScale", DATATYPE_INTEGER, true, null, "0", String.valueOf(Integer.MAX_VALUE), true, true);
// root -> ImageResources -> PrintScale
addElement("PrintScale", "ImageResources", CHILD_POLICY_EMPTY);
addAttribute("PrintScale", "style", DATATYPE_STRING, false, null, Arrays.asList(PSDMetadata.PRINT_SCALE_STYLES));
addAttribute("PrintScale", "xLocation", DATATYPE_FLOAT, true, null);
addAttribute("PrintScale", "yLocation", DATATYPE_FLOAT, true, null);
addAttribute("PrintScale", "scale", DATATYPE_FLOAT, true, null);
// root -> ImageResources -> ResolutionInfo
addElement("ResolutionInfo", "ImageResources", CHILD_POLICY_EMPTY);
addAttribute("ResolutionInfo", "hRes", DATATYPE_FLOAT, true, null);
addAttribute("ResolutionInfo", "hResUnit", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.RESOLUTION_UNITS));
addAttribute("ResolutionInfo", "widthUnit", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.DIMENSION_UNITS));
addAttribute("ResolutionInfo", "vRes", DATATYPE_FLOAT, true, null);
addAttribute("ResolutionInfo", "vResUnit", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.RESOLUTION_UNITS));
addAttribute("ResolutionInfo", "heightUnit", DATATYPE_STRING, true, null, Arrays.asList(PSDMetadata.DIMENSION_UNITS));
// root -> ImageResources -> UnicodeAlphaNames
addElement("UnicodeAlphaNames", "ImageResources", 0, Integer.MAX_VALUE);
addChildElement("UnicodeAlphaNames", "Name"); // TODO: Does this really work?
// root -> ImageResources -> VersionInfo
addElement("VersionInfo", "ImageResources", CHILD_POLICY_EMPTY);
addAttribute("VersionInfo", "version", DATATYPE_INTEGER, false, "1");
addBooleanAttribute("VersionInfo", "hasRealMergedData", false, false);
addAttribute("VersionInfo", "writer", DATATYPE_STRING, true, null);
addAttribute("VersionInfo", "reader", DATATYPE_STRING, true, null);
addAttribute("VersionInfo", "fileVersion", DATATYPE_INTEGER, true, "1");
// root -> ImageResources -> Thumbnail
addElement("Thumbnail", "ImageResources", CHILD_POLICY_EMPTY);
addObjectValue("Thumbnail", BufferedImage.class, true, null);
// root -> ImageResources -> UnicodeAlphaName
addElement("UnicodeAlphaName", "ImageResources", CHILD_POLICY_EMPTY);
addAttribute("UnicodeAlphaName", "value", DATATYPE_STRING, true, null);
// root -> ImageResources -> XMP
addElement("XMP", "ImageResources", CHILD_POLICY_CHOICE);
// TODO: Incorporate XMP metadata here somehow (or treat as opaque bytes?)
addObjectValue("XMP", Document.class, true, null);
// TODO: Layers
//addElement("ChannelSourceDestinationRange", "LayerSomething", CHILD_POLICY_CHOICE);
// TODO: Global layer mask info
}
@Override
public boolean canNodeAppear(final String pElementName, final ImageTypeSpecifier pImageType) {
// TODO: PSDColorData and PaletteEntry only for indexed color model
throw new UnsupportedOperationException("Method canNodeAppear not implemented"); // TODO: Implement
}
/**
* Returns the shared instance of the {@code PSDMetadataFormat}.
*
* @return the shared instance.
* @see javax.imageio.metadata.IIOMetadata#getMetadataFormat
*/
public static PSDMetadataFormat getInstance() {
return sInstance;
}
}

View File

@ -0,0 +1,27 @@
package com.twelvemonkeys.imageio.plugins.psd;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
/**
* PSDPixelAspectRatio
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDPixelAspectRatio.java,v 1.0 Nov 7, 2009 8:23:09 PM haraldk Exp$
*/
final class PSDPixelAspectRatio extends PSDImageResource {
// 4 bytes (version = 1), 8 bytes double, x / y of a pixel
int mVersion;
double mAspect;
PSDPixelAspectRatio(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
}
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
mVersion = pInput.readInt();
mAspect = pInput.readDouble();
}
}

View File

@ -11,14 +11,14 @@ import java.io.IOException;
* @version $Id: PSDPrintFlagsInfo.java,v 1.0 Jul 28, 2009 5:16:27 PM haraldk Exp$ * @version $Id: PSDPrintFlagsInfo.java,v 1.0 Jul 28, 2009 5:16:27 PM haraldk Exp$
*/ */
final class PSDPrintFlags extends PSDImageResource { final class PSDPrintFlags extends PSDImageResource {
private boolean mLabels; boolean mLabels;
private boolean mCropMasks; boolean mCropMasks;
private boolean mColorBars; boolean mColorBars;
private boolean mRegistrationMarks; boolean mRegistrationMarks;
private boolean mNegative; boolean mNegative;
private boolean mFlip; boolean mFlip;
private boolean mInterpolate; boolean mInterpolate;
private boolean mCaption; boolean mCaption;
PSDPrintFlags(final short pId, final ImageInputStream pInput) throws IOException { PSDPrintFlags(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput); super(pId, pInput);
@ -26,14 +26,14 @@ final class PSDPrintFlags extends PSDImageResource {
@Override @Override
protected void readData(final ImageInputStream pInput) throws IOException { protected void readData(final ImageInputStream pInput) throws IOException {
mLabels = pInput.readUnsignedByte() != 0; mLabels = pInput.readBoolean();
mCropMasks = pInput.readUnsignedByte() != 0; mCropMasks = pInput.readBoolean();
mColorBars = pInput.readUnsignedByte() != 0; mColorBars = pInput.readBoolean();
mRegistrationMarks = pInput.readUnsignedByte() != 0; mRegistrationMarks = pInput.readBoolean();
mNegative = pInput.readUnsignedByte() != 0; mNegative = pInput.readBoolean();
mFlip = pInput.readUnsignedByte() != 0; mFlip = pInput.readBoolean();
mInterpolate = pInput.readUnsignedByte() != 0; mInterpolate = pInput.readBoolean();
mCaption = pInput.readUnsignedByte() != 0; mCaption = pInput.readBoolean();
pInput.skipBytes(mSize - 8); pInput.skipBytes(mSize - 8);
} }

View File

@ -11,11 +11,11 @@ import java.io.IOException;
* @version $Id: PSDPrintFlagsInfo.java,v 1.0 Jul 28, 2009 5:16:27 PM haraldk Exp$ * @version $Id: PSDPrintFlagsInfo.java,v 1.0 Jul 28, 2009 5:16:27 PM haraldk Exp$
*/ */
final class PSDPrintFlagsInformation extends PSDImageResource { final class PSDPrintFlagsInformation extends PSDImageResource {
private int mVersion; int mVersion;
private boolean mCropMasks; boolean mCropMasks;
private int mField; int mField;
private long mBleedWidth; long mBleedWidth;
private int mBleedScale; int mBleedScale;
PSDPrintFlagsInformation(final short pId, final ImageInputStream pInput) throws IOException { PSDPrintFlagsInformation(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput); super(pId, pInput);
@ -24,8 +24,8 @@ final class PSDPrintFlagsInformation extends PSDImageResource {
@Override @Override
protected void readData(final ImageInputStream pInput) throws IOException { protected void readData(final ImageInputStream pInput) throws IOException {
mVersion = pInput.readUnsignedShort(); mVersion = pInput.readUnsignedShort();
mCropMasks = pInput.readUnsignedByte() != 0; mCropMasks = pInput.readBoolean();
mField = pInput.readUnsignedByte(); mField = pInput.readUnsignedByte(); // TODO: Is this really pad?
mBleedWidth = pInput.readUnsignedInt(); mBleedWidth = pInput.readUnsignedInt();
mBleedScale = pInput.readUnsignedShort(); mBleedScale = pInput.readUnsignedShort();

View File

@ -0,0 +1,35 @@
package com.twelvemonkeys.imageio.plugins.psd;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
/**
* PSDPrintScale
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDPrintScale.java,v 1.0 Nov 7, 2009 9:41:17 PM haraldk Exp$
*/
final class PSDPrintScale extends PSDImageResource {
// 2 bytes style (0 = centered, 1 = size to fit, 2 = user defined).
// 4 bytes x location (floating point).
// 4 bytes y location (floating point).
// 4 bytes scale (floating point)
short mStyle;
float mXLocation;
float mYlocation;
float mScale;
PSDPrintScale(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
}
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
mStyle = pInput.readShort();
mXLocation = pInput.readFloat();
mYlocation = pInput.readFloat();
mScale = pInput.readFloat();
}
}

View File

@ -50,13 +50,12 @@ class PSDResolutionInfo extends PSDImageResource {
// WORD HeightUnit; /* 1=in, 2=cm, 3=pt, 4=picas, 5=columns */ // WORD HeightUnit; /* 1=in, 2=cm, 3=pt, 4=picas, 5=columns */
// } RESOLUTIONINFO; // } RESOLUTIONINFO;
private float mHRes; float mHRes;
private short mHResUnit; short mHResUnit;
private short mWidthUnit; short mWidthUnit;
private float mVRes; float mVRes;
private short mVResUnit; short mVResUnit;
private short mHeightUnit; short mHeightUnit;
PSDResolutionInfo(final short pId, final ImageInputStream pInput) throws IOException { PSDResolutionInfo(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput); super(pId, pInput);

View File

@ -37,7 +37,7 @@ class PSDThumbnail extends PSDImageResource {
*/ */
@Override @Override
protected void readData(final ImageInputStream pInput) throws IOException { protected void readData(final ImageInputStream pInput) throws IOException {
// TODO: Support for RAW RGB (format == 0) // TODO: Support for RAW RGB (format == 0): Extract RAW reader from PICT RAW QuickTime decompressor
int format = pInput.readInt(); int format = pInput.readInt();
switch (format) { switch (format) {
case 0: case 0:

View File

@ -0,0 +1,33 @@
package com.twelvemonkeys.imageio.plugins.psd;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* PSDUnicodeAlphaNames
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDUnicodeAlphaNames.java,v 1.0 Nov 7, 2009 9:16:56 PM haraldk Exp$
*/
final class PSDUnicodeAlphaNames extends PSDImageResource {
List<String> mNames;
PSDUnicodeAlphaNames(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
}
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
mNames = new ArrayList<String>();
long left = mSize;
while (left > 0) {
String name = PSDUtil.readUTF16String(pInput);
mNames.add(name);
left -= name.length() * 2 + 4;
}
}
}

View File

@ -31,8 +31,10 @@ package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.io.enc.DecoderStream; import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder; import com.twelvemonkeys.io.enc.PackBitsDecoder;
import com.twelvemonkeys.lang.StringUtil;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.io.DataInput;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
@ -58,22 +60,21 @@ final class PSDUtil {
} }
// TODO: Proably also useful for PICT reader, move to some common util? // TODO: Proably also useful for PICT reader, move to some common util?
static String readPascalString(ImageInputStream pInput) throws IOException { // 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(); int length = pInput.readUnsignedByte();
// int length = pInput.readUnsignedShort();
byte[] bytes = new byte[length]; byte[] bytes = new byte[length];
pInput.readFully(bytes); pInput.readFully(bytes);
if (length % 2 == 0) {
pInput.readByte(); // Pad return StringUtil.decode(bytes, 0, bytes.length, "ASCII");
}
return new String(bytes);
} }
static String readPascalStringByte(ImageInputStream pInput) throws IOException { static String readUTF16String(final DataInput pInput) throws IOException {
int length = pInput.readUnsignedByte(); int length = pInput.readInt();
byte[] bytes = new byte[length]; byte[] bytes = new byte[length * 2];
pInput.readFully(bytes); pInput.readFully(bytes);
return new String(bytes);
return StringUtil.decode(bytes, 0, bytes.length, "UTF-16");
} }
static DataInputStream createPackBitsStream(final ImageInputStream pInput, long pLength) { static DataInputStream createPackBitsStream(final ImageInputStream pInput, long pLength) {

View File

@ -0,0 +1,57 @@
package com.twelvemonkeys.imageio.plugins.psd;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
/**
* PSDVersionInfo
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDVersionInfo.java,v 1.0 Nov 6, 2009 1:02:19 PM haraldk Exp$
*/
final class PSDVersionInfo extends PSDImageResource {
int mVersion;
boolean mHasRealMergedData;
String mWriter;
String mReader;
int mFileVersion;
PSDVersionInfo(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
}
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
/*
4 bytes version
1 byte hasRealMergedData
Unicode string: writer name
Unicode string: reader name
4 bytes file version.
*/
mVersion = pInput.readInt();
mHasRealMergedData = pInput.readBoolean();
mWriter = PSDUtil.readUTF16String(pInput);
mReader = PSDUtil.readUTF16String(pInput);
mFileVersion = pInput.readInt();
}
@Override
public String toString() {
StringBuilder builder = toStringBuilder();
builder.append(", version: ").append(mVersion);
builder.append(", hasRealMergedData: ").append(mHasRealMergedData);
builder.append(", writer: ").append(mWriter);
builder.append(", reader: ").append(mReader);
builder.append(", file version: ").append(mFileVersion);
builder.append("]");
return builder.toString();
}
}

View File

@ -2,4 +2,5 @@ Implement source subsampling and region of interest
Separate package for the resources (seems to be a lot)? Separate package for the resources (seems to be a lot)?
Possibility to read only some resources? readResources(int[] resourceKeys)? Possibility to read only some resources? readResources(int[] resourceKeys)?
- Probably faster when we only need the color space - Probably faster when we only need the color space
PSDImageWriter Support for Photoshop specific TIFF tags (extension for TIFFImageReader)?
PSDImageWriter?

View File

@ -11,4 +11,6 @@
- http://vinetto.sourceforge.net/docs.html - http://vinetto.sourceforge.net/docs.html
- We probably want to support all of these - We probably want to support all of these
- Thumbnail (API) support? Does it make sense? It's all thumbnails..
DONE: DONE:

View File

@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>com.twelvemonkeys</groupId> <groupId>com.twelvemonkeys</groupId>
<artifactId>twelvemonkeys-servlet</artifactId> <artifactId>twelvemonkeys-servlet</artifactId>
<version>2.2</version> <version>2.3-SNAPSHOT</version>
<name>TwelveMonkeys Servlet</name> <name>TwelveMonkeys Servlet</name>
<parent> <parent>
@ -89,6 +89,22 @@
<encoding>UTF-8</encoding> <encoding>UTF-8</encoding>
</configuration> </configuration>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.2</version>
<configuration>
<archive>
<manifestEntries>
<Implementation-Title>${project.name}</Implementation-Title>
<Implementation-Vendor>TwelveMonkeys</Implementation-Vendor>
<Implementation-Version>${project.version}</Implementation-Version>
<Implementation-URL>http://github.com/haraldk/TwelveMonkeys</Implementation-URL>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins> </plugins>
</build> </build>