Work in progress for PSD metadata support:

- Added PSDMetadata and PSDMetadataFormat
 - Implemented most of standard format
 - Start of native format definintion
 - Updated SPI and Reader to return new format
This commit is contained in:
Harald Kuhr 2009-11-06 09:26:47 +08:00 committed by Erlend Hamnaberg
parent 34d874d69d
commit b5f6c96583
9 changed files with 748 additions and 44 deletions

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

@ -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,7 @@ 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
//typedef _DisplayInfo //typedef _DisplayInfo
//{ //{
// WORD ColorSpace; // WORD ColorSpace;
@ -67,20 +67,20 @@ class PSDDisplayInfo extends PSDImageResource {
// long left = mSize; // long left = mSize;
// while (left > 0) { // while (left > 0) {
mColorSpace = pInput.readShort(); mColorSpace = pInput.readShort();
// Color[4]...? // Color[4]...?
mColors = new short[4]; mColors = new short[4];
mColors[0] = pInput.readShort(); mColors[0] = pInput.readShort();
mColors[1] = pInput.readShort(); mColors[1] = pInput.readShort();
mColors[2] = pInput.readShort(); mColors[2] = pInput.readShort();
mColors[3] = pInput.readShort(); mColors[3] = pInput.readShort();
mOpacity = pInput.readShort(); mOpacity = pInput.readShort();
mKind = pInput.readByte(); mKind = pInput.readByte();
pInput.readByte(); // Pad pInput.readByte(); // Pad
// left -= 14; // left -= 14;
// } // }
pInput.skipBytes(mSize - 14); pInput.skipBytes(mSize - 14);

View File

@ -35,7 +35,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];

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,7 +62,7 @@ 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
@ -115,7 +120,7 @@ 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(mColorData.getIndexColorModel());
} }
@ -184,6 +189,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)
@ -404,7 +414,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 +546,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,
@ -697,6 +708,7 @@ public class PSDImageReader extends ImageReaderBase {
mColorData = new PSDColorData(mImageInput); 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 +720,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();
@ -735,6 +748,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();
@ -767,7 +782,7 @@ 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());
} }
@ -795,6 +810,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 +854,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
@ -892,7 +908,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 +947,57 @@ 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);
PSDMetadata metadata = new PSDMetadata();
metadata.mHeader = mHeader;
metadata.mColorData = mColorData;
metadata.mImageResources = mImageResources;
return metadata;
}
@Override
public IIOMetadata getImageMetadata(final int imageIndex, final String formatName, final Set<String> nodeNames) throws IOException {
// TODO: This might make sense, 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() {
@ -965,13 +1032,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 +1049,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 +1068,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,12 +1120,17 @@ 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.mImageResources);
imageReader.readLayerAndMaskInfo(true); imageReader.readLayerAndMaskInfo(true);
System.out.println("imageReader.mLayerInfo: " + imageReader.mLayerInfo); System.out.println("imageReader.mLayerInfo: " + imageReader.mLayerInfo);
// System.out.println("imageReader.mGlobalLayerMask: " + imageReader.mGlobalLayerMask); // System.out.println("imageReader.mGlobalLayerMask: " + imageReader.mGlobalLayerMask);
IIOMetadata metadata = imageReader.getImageMetadata(0);
Node node = metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
XMLSerializer 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);
for (int i = 0; i < thumbnails; i++) { for (int i = 0; i < thumbnails; i++) {

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

@ -0,0 +1,446 @@
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.lang.StringUtil;
import org.w3c.dom.Node;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import java.awt.image.IndexColorModel;
import java.util.ArrayList;
import java.util.Arrays;
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 {
static final String NATIVE_METADATA_FORMAT_NAME = "com_twelvemonkeys_imageio_psd_1.0";
static final String NATIVE_METADATA_FORMAT_CLASS_NAME = "com.twelvemonkeys.imageio.plugins.psd.PSDMetadataFormat";
// TODO: Move fields from PSDImageReader (header, color map, resources, etc) here
PSDHeader mHeader;
PSDColorData mColorData;
List<PSDImageResource> mImageResources;
PSDGlobalLayerMask mGlobalLayerMask;
List<PSDLayerInfo> mLayerInfo;
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);
}
}
private Node getNativeTree() {
throw new UnsupportedOperationException("getNativeTree");
}
/// 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 is 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:
// TODO: FixMe
cs = "???";
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...
node = new IIOMetadataNode("NumChannels");
node.setAttribute("value", Integer.toString(mHeader.mChannels));
chroma_node.appendChild(node);
// if (gAMA_present) {
// node = new IIOMetadataNode("Gamma");
// node.setAttribute("value", Float.toString(gAMA_gamma*1.0e-5F));
// 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 = 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);
}
chroma_node.appendChild(node);
}
// 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;
}
@Override
protected IIOMetadataNode getStandardCompressionNode() {
IIOMetadataNode compression_node = new IIOMetadataNode("Compression");
IIOMetadataNode node; // scratch node
node = new IIOMetadataNode("CompressionTypeName");
// TODO: Only if set...
node.setAttribute("value", "PackBits");
compression_node.appendChild(node);
node = new IIOMetadataNode("Lossless");
node.setAttribute("value", "true");
compression_node.appendChild(node);
// 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);
List<PSDResolutionInfo> resolutionInfos = getResources(PSDResolutionInfo.class);
if (!resolutionInfos.isEmpty()) {
PSDResolutionInfo resolutionInfo = resolutionInfos.get(0);
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() {
// TODO: PSDVersionInfo
// if (!tIME_present) {
// return null;
// }
//
// IIOMetadataNode document_node = new IIOMetadataNode("Document");
// IIOMetadataNode node = null; // scratch node
//
// node = new IIOMetadataNode("ImageModificationTime");
// node.setAttribute("year", Integer.toString(tIME_year));
// node.setAttribute("month", Integer.toString(tIME_month));
// node.setAttribute("day", Integer.toString(tIME_day));
// node.setAttribute("hour", Integer.toString(tIME_hour));
// node.setAttribute("minute", Integer.toString(tIME_minute));
// node.setAttribute("second", Integer.toString(tIME_second));
// document_node.appendChild(node);
//
// return document_node;
return null;
}
@Override
protected IIOMetadataNode getStandardTextNode() {
// TODO: CaptionDigest?, EXIF, XMP
// 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;
}
// TODO: Replace with filter iterator?
<T extends PSDImageResource> List<T> getResources(final Class<T> pResourceType) {
List<T> filtered = null;
for (PSDImageResource resource : mImageResources) {
if (pResourceType.isInstance(resource)) {
if (filtered == null) {
filtered = new ArrayList<T>();
}
filtered.add(pResourceType.cast(resource));
}
}
return filtered;
}
}

View File

@ -0,0 +1,165 @@
package com.twelvemonkeys.imageio.plugins.psd;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadataFormatImpl;
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("PSDHeader", PSDMetadata.NATIVE_METADATA_FORMAT_NAME, CHILD_POLICY_EMPTY);
// TODO: Do the first two make sense?
addAttribute("PSDHeader", "signature", DATATYPE_STRING, false, "8BPS", Arrays.asList("8BPS"));
addAttribute("PSDHeader", "version", DATATYPE_INTEGER, false, "1", Arrays.asList("1"));
addAttribute("PSDHeader", "channels", DATATYPE_INTEGER, true, null, "1", "24", true, true);
// rows?
addAttribute("PSDHeader", "height", DATATYPE_INTEGER, true, null, "1", "30000", true, true);
// columns?
addAttribute("PSDHeader", "width", DATATYPE_INTEGER, true, null, "1", "30000", true, true);
addAttribute("PSDHeader", "bits", DATATYPE_INTEGER, true, null, Arrays.asList("1", "8", "16"));
addAttribute("PSDHeader", "mode", DATATYPE_INTEGER, true, null, Arrays.asList(
String.valueOf(PSD.COLOR_MODE_MONOCHROME),
String.valueOf(PSD.COLOR_MODE_GRAYSCALE),
String.valueOf(PSD.COLOR_MODE_INDEXED),
String.valueOf(PSD.COLOR_MODE_RGB),
String.valueOf(PSD.COLOR_MODE_CMYK),
String.valueOf(PSD.COLOR_MODE_MULTICHANNEL),
String.valueOf(PSD.COLOR_MODE_DUOTONE),
String.valueOf(PSD.COLOR_MODE_LAB)
));
/*
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", "PSDColorData", 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", CHILD_POLICY_EMPTY);
addAttribute("AlphaChannelInfo", "names", DATATYPE_STRING, true, 0, Integer.MAX_VALUE);
// root -> ImageResources -> DisplayInfo
addElement("DisplayInfo", "ImageResources", CHILD_POLICY_EMPTY);
addAttribute("DisplayInfo", "colorSpace", DATATYPE_INTEGER, true, null);
addAttribute("DisplayInfo", "colors", DATATYPE_INTEGER, true, 4, 4);
addAttribute("DisplayInfo", "opacity", DATATYPE_INTEGER, true, null, "0", "100", true, true);
addAttribute("DisplayInfo", "kind", DATATYPE_INTEGER, true, null, Arrays.asList("0", "1"));
// root -> ImageResources -> EXIF1Data
addElement("EXIF1Data", "ImageResources", CHILD_POLICY_ALL);
// TODO: Incorporate EXIF / TIFF metadata here somehow... (or treat as opaque bytes?)
// 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, true, null);
addBooleanAttribute("PrintFlagsInformation", "cropMarks", false, false);
addAttribute("PrintFlagsInformation", "field", DATATYPE_INTEGER, true, null);
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 -> ResolutionInfo
addElement("ResolutionInfo", "ImageResources", CHILD_POLICY_EMPTY);
addAttribute("ResolutionInfo", "hRes", DATATYPE_FLOAT, true, null);
// TODO: Or use string and more friendly names? "pixels/inch"/"pixels/cm" and "inch"/"cm"/"pt"/"pica"/"column"
addAttribute("ResolutionInfo", "hResUnit", DATATYPE_INTEGER, true, null, Arrays.asList("1", "2"));
addAttribute("ResolutionInfo", "widthUnit", DATATYPE_INTEGER, true, null, Arrays.asList("1", "2", "3", "4", "5"));
addAttribute("ResolutionInfo", "vRes", DATATYPE_FLOAT, true, null);
// TODO: Or use more friendly names?
addAttribute("ResolutionInfo", "vResUnit", DATATYPE_INTEGER, true, null, Arrays.asList("1", "2"));
addAttribute("ResolutionInfo", "heightUnit", DATATYPE_INTEGER, true, null, Arrays.asList("1", "2", "3", "4", "5"));
// ??? addElement("Thumbnail", "ImageResources", CHILD_POLICY_CHOICE);
// root -> ImageResources -> XMPData
addElement("XMPData", "ImageResources", CHILD_POLICY_CHOICE);
// TODO: Incorporate XMP metadata here somehow (or treat as opaque bytes?)
// 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

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