Layer support for PSD format. Still work in progress, but the basics is done.

This commit is contained in:
Harald Kuhr 2011-10-30 21:54:47 +01:00
parent 5857e27cf2
commit 8edc448bf9
5 changed files with 409 additions and 140 deletions

View File

@ -62,6 +62,7 @@ class ICCProfile extends PSDImageResource {
}
public ICC_Profile getProfile() {
// Warning: Mutable...
return profile;
}
@ -74,5 +75,4 @@ class ICCProfile extends PSDImageResource {
return builder.toString();
}
}

View File

@ -63,14 +63,13 @@ import java.util.List;
* @version $Id: PSDImageReader.java,v 1.0 Apr 29, 2008 4:45:52 PM haraldk Exp$
*/
// TODO: Implement ImageIO meta data interface
// TODO: Allow reading the extra alpha channels (index after composite data)
// TODO: Figure out of we should assume Adobe RGB (1998) color model, if no embedded profile?
// TODO: Support for PSDVersionInfo hasRealMergedData=false (no real composite data, layers will be in index 0)
// TODO: Support for API for reading separate layers (index after composite data, and optional alpha channels)
// TODO: Consider Romain Guy's Java 2D implementation of PS filters for the blending modes in layers
// http://www.curious-creature.org/2006/09/20/new-blendings-modes-for-java2d/
// See http://www.codeproject.com/KB/graphics/PSDParser.aspx
// See http://www.adobeforums.com/webx?14@@.3bc381dc/0
// See http://www.adobeforums.com/webx?14@@.3bc381dc/0
// Done: Allow reading the extra alpha channels (index after composite data)
public class PSDImageReader extends ImageReaderBase {
private PSDHeader header;
private ICC_ColorSpace colorSpace;
@ -89,15 +88,47 @@ public class PSDImageReader extends ImageReaderBase {
public int getWidth(final int imageIndex) throws IOException {
checkBounds(imageIndex);
readHeader();
if (imageIndex > 0) {
return getLayerWidth(imageIndex - 1);
}
return header.width;
}
public int getHeight(final int imageIndex) throws IOException {
checkBounds(imageIndex);
readHeader();
if (imageIndex > 0) {
return getLayerHeight(imageIndex - 1);
}
return header.height;
}
private int getLayerWidth(int layerIndex) throws IOException {
if (metadata == null || metadata.layerInfo == null) {
readImageResources(false);
readLayerAndMaskInfo(true);
}
PSDLayerInfo layerInfo = metadata.layerInfo.get(layerIndex);
return layerInfo.right - layerInfo.left;
}
private int getLayerHeight(int layerIndex) throws IOException {
if (metadata == null || metadata.layerInfo == null) {
readImageResources(false);
readLayerAndMaskInfo(true);
}
PSDLayerInfo layerInfo = metadata.layerInfo.get(layerIndex);
return layerInfo.bottom - layerInfo.top;
}
@Override
public ImageTypeSpecifier getRawImageType(final int imageIndex) throws IOException {
return getRawImageTypeInternal(imageIndex);
@ -107,8 +138,22 @@ public class PSDImageReader extends ImageReaderBase {
checkBounds(imageIndex);
readHeader();
ColorSpace cs;
// Image index above 0, means a layer
if (imageIndex > 0) {
if (metadata == null || metadata.layerInfo == null) {
readImageResources(false);
readLayerAndMaskInfo(true);
}
return getRawImageTypeForLayer(imageIndex - 1);
}
// Otherwise, get the type specifier for the composite layer
return getRawImageTypeForCompositeLayer();
}
private ImageTypeSpecifier getRawImageTypeForCompositeLayer() throws IOException {
ColorSpace cs;
switch (header.mode) {
case PSD.COLOR_MODE_MONOCHROME:
if (header.channels == 1 && header.bits == 1) {
@ -147,8 +192,8 @@ public class PSDImageReader extends ImageReaderBase {
case PSD.COLOR_MODE_RGB:
cs = getEmbeddedColorSpace();
if (cs == null) {
// TODO: Should probably be Adobe RGB (1998), not sRGB. Or..?
cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
// TODO: Should probably be Adobe RGB (1998), not sRGB. Or..? Can't find any spec saying either...
cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); // ColorSpaces.getColorSpace(ColorSpaces.CS_ADOBE_RGB_1998); ?
}
if (header.channels == 3 && header.bits == 8) {
@ -216,7 +261,8 @@ public class PSDImageReader extends ImageReaderBase {
switch (header.mode) {
case PSD.COLOR_MODE_RGB:
// Prefer interleaved versions as they are much faster to display
if (header.channels == 3 && header.bits == 8) {
// if (header.channels == 3 && header.bits == 8) {
if (rawType.getNumBands() == 3 && rawType.getBitsPerBand(0) == 8) {
// TODO: ColorConvertOp to CS_sRGB
// TODO: Integer raster
// types.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.INT_RGB));
@ -227,7 +273,8 @@ public class PSDImageReader extends ImageReaderBase {
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {2, 1, 0}, DataBuffer.TYPE_BYTE, false, false));
}
}
else if (header.channels >= 4 && header.bits == 8) {
// else if (header.channels >= 4 && header.bits == 8) {
else if (rawType.getNumBands() >= 4 && rawType.getBitsPerBand(0) == 8) {
// TODO: ColorConvertOp to CS_sRGB
// TODO: Integer raster
// types.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.INT_ARGB));
@ -238,10 +285,12 @@ public class PSDImageReader extends ImageReaderBase {
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, true, false));
}
}
else if (header.channels == 3 && header.bits == 16) {
// else if (header.channels == 3 && header.bits == 16) {
else if (rawType.getNumBands() == 3 && rawType.getBitsPerBand(0) == 16) {
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {2, 1, 0}, DataBuffer.TYPE_USHORT, false, false));
}
else if (header.channels >= 4 && header.bits == 16) {
// else if (header.channels >= 4 && header.bits == 16) {
else if (rawType.getNumBands() >= 4 && rawType.getBitsPerBand(0) == 16) {
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_USHORT, true, false));
}
break;
@ -252,16 +301,20 @@ public class PSDImageReader extends ImageReaderBase {
// as Java2D is extremely slow displaying custom images.
// Converting to RGB is also correct behaviour, according to the docs.
// Doing this, will require rewriting the image reading, as the raw image data is channelled, not interleaved :-/
if (header.channels == 4 && header.bits == 8) {
// if (header.channels == 4 && header.bits == 8) {
if (rawType.getNumBands() == 4 && rawType.getBitsPerBand(0) == 8) {
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false));
}
else if (header.channels == 5 && header.bits == 8) {
// else if (header.channels == 5 && header.bits == 8) {
else if (rawType.getNumBands() == 5 && rawType.getBitsPerBand(0) == 8) {
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {4, 3, 2, 1, 0}, DataBuffer.TYPE_BYTE, true, false));
}
else if (header.channels == 4 && header.bits == 16) {
// else if (header.channels == 4 && header.bits == 16) {
else if (rawType.getNumBands() == 4 && rawType.getBitsPerBand(0) == 16) {
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[]{3, 2, 1, 0}, DataBuffer.TYPE_USHORT, false, false));
}
else if (header.channels == 5 && header.bits == 16) {
// else if (header.channels == 5 && header.bits == 16) {
else if (rawType.getNumBands() == 5 && rawType.getBitsPerBand(0) == 16) {
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {4, 3, 2, 1, 0}, DataBuffer.TYPE_USHORT, true, false));
}
break;
@ -301,7 +354,26 @@ public class PSDImageReader extends ImageReaderBase {
readHeader();
readImageResources(false);
readLayerAndMaskInfo(false);
// readLayerAndMaskInfo(false);
readLayerAndMaskInfo(imageIndex > 0);
// TODO: What about the extra alpha channels possibly present? Read as gray scale as extra images?
// Layer hacks... For now, any index above 0 is considered to be a layer...
// TODO: Support layer in index 0, if "has real merged data" flag is false?
// TODO: Param support in layer code (more duping/cleanup..)
if (imageIndex > 0) {
// ImageTypeSpecifier compositeType = getRawImageTypeForCompositeLayer();
// ImageTypeSpecifier imageType = getImageTypes(0).next();
// int layerIndex = imageIndex - 1;
// PSDLayerInfo layerInfo = metadata.layerInfo.get(layerIndex);
//
// imageInput.seek(findLayerStartPos(layerIndex));
// return readLayerData(layerIndex, layerInfo, compositeType, imageType, param);
return readLayerData(imageIndex - 1, param);
}
BufferedImage image = getDestination(param, getImageTypes(imageIndex), header.width, header.height);
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
@ -371,7 +443,7 @@ public class PSDImageReader extends ImageReaderBase {
case PSD.COMPRESSION_ZIP_PREDICTION:
// TODO: Need to find out if the normal java.util.zip can handle this...
// Could be same as PNG prediction? Read up...
throw new IIOException("ZIP compression not supported yet");
throw new IIOException("PSD with ZIP compression not supported");
default:
throw new IIOException(
String.format(
@ -394,6 +466,20 @@ public class PSDImageReader extends ImageReaderBase {
return image;
}
private long findLayerStartPos(int layerIndex) {
long layersStart = metadata.layersStart;
for (int i = 0; i < layerIndex; i++) {
PSDLayerInfo layerInfo = metadata.layerInfo.get(i);
for (PSDChannelInfo channelInfo : layerInfo.channelInfo) {
layersStart += channelInfo.length;
}
}
return layersStart;
}
private void readImageData(final BufferedImage pImage,
final ColorModel pSourceCM, final Rectangle pSource, final Rectangle pDest,
final int pXSub, final int pYSub,
@ -539,7 +625,6 @@ public class PSDImageReader extends ImageReaderBase {
imageInput.readFully(pRow, 0, pChannelWidth);
}
// TODO: If banded and not sub sampling/cmyk, we could just copy using System.arraycopy
// TODO: Destination offset...??
// Copy line sub sampled into real data
int offset = (y - pSource.y) / pYSub * pDest.width * pBands + pBandOffset;
@ -652,7 +737,6 @@ public class PSDImageReader extends ImageReaderBase {
// TODO: Probably faster to do this in line..
if (pBuffer.getNumBanks() > 1) {
for (int y = 0; y < pHeight; y++) {
for (int x = 0; x < pWidth; x++) {
int offset = (x + y * pWidth);
@ -772,55 +856,44 @@ public class PSDImageReader extends ImageReaderBase {
// TODO: Obey ignoreMetadata
private void readLayerAndMaskInfo(final boolean pParseData) throws IOException {
// TODO: Make sure we are positioned correctly
// TODO: Avoid unnecessary stream repositioning
long length = imageInput.readUnsignedInt();
if (pParseData && length > 0) {
long pos = imageInput.getStreamPosition();
long layerInfoLength = imageInput.readUnsignedInt();
long read;
if (metadata.layerInfo == null) {
long layerInfoLength = imageInput.readUnsignedInt();
/*
"Layer count. If it is a negative number, its absolute value is the number of
layers and the first alpha channel contains the transparency data for the
merged result."
*/
// TODO: Figure out what the last part of that sentence means in practice...
int layers = imageInput.readShort();
/*
"Layer count. If it is a negative number, its absolute value is the number of
layers and the first alpha channel contains the transparency data for the
merged result."
*/
// TODO: Figure out what the last part of that sentence means in practice...
int layers = imageInput.readShort();
PSDLayerInfo[] layerInfos = new PSDLayerInfo[Math.abs(layers)];
for (int i = 0; i < layerInfos.length; i++) {
layerInfos[i] = new PSDLayerInfo(imageInput);
}
metadata.layerInfo = Arrays.asList(layerInfos);
PSDLayerInfo[] layerInfos = new PSDLayerInfo[Math.abs(layers)];
for (int i = 0; i < layerInfos.length; i++) {
layerInfos[i] = new PSDLayerInfo(imageInput);
}
metadata.layerInfo = Arrays.asList(layerInfos);
metadata.layersStart = imageInput.getStreamPosition();
// TODO: Clean-up
imageInput.mark();
ImageTypeSpecifier raw = getRawImageTypeInternal(0);
ImageTypeSpecifier imageType = getImageTypes(0).next();
imageInput.reset();
read = imageInput.getStreamPosition() - pos;
for (PSDLayerInfo layerInfo : layerInfos) {
// TODO: If not explicitly needed, skip layers...
BufferedImage layer = readLayerData(layerInfo, raw, imageType);
long diff = layerInfoLength - (read - 4); // - 4 for the layerInfoLength field itself
// System.out.println("diff: " + diff);
imageInput.skipBytes(diff);
// TODO: Don't show! Store in meta data somehow...
// if (layer != null) {
// showIt(layer, layerInfo.layerName + " " + layerInfo.blendMode.toString());
// }
}
long read = imageInput.getStreamPosition() - pos;
long diff = layerInfoLength - (read - 4); // - 4 for the layerInfoLength field itself
// System.out.println("diff: " + diff);
imageInput.skipBytes(diff);
// TODO: Global LayerMaskInfo (18 bytes or more..?)
// 4 (length), 2 (colorSpace), 8 (4 * 2 byte color components), 2 (opacity %), 1 (kind), variable (pad)
long layerMaskInfoLength = imageInput.readUnsignedInt();
// System.out.println("GlobalLayerMaskInfo length: " + layerMaskInfoLength);
if (layerMaskInfoLength > 0) {
metadata.globalLayerMask = new PSDGlobalLayerMask(imageInput);
// System.out.println("globalLayerMask: " + globalLayerMask);
// TODO: Global LayerMaskInfo (18 bytes or more..?)
// 4 (length), 2 (colorSpace), 8 (4 * 2 byte color components), 2 (opacity %), 1 (kind), variable (pad)
long layerMaskInfoLength = imageInput.readUnsignedInt();
// System.out.println("GlobalLayerMaskInfo length: " + layerMaskInfoLength);
if (layerMaskInfoLength > 0) {
metadata.globalLayerMask = new PSDGlobalLayerMask(imageInput);
// System.err.println("globalLayerMask: " + metadata.globalLayerMask);
}
}
read = imageInput.getStreamPosition() - pos;
@ -835,15 +908,28 @@ public class PSDImageReader extends ImageReaderBase {
}
}
private BufferedImage readLayerData(final PSDLayerInfo pLayerInfo, final ImageTypeSpecifier pRawType, final ImageTypeSpecifier pImageType) throws IOException {
final int width = pLayerInfo.right - pLayerInfo.left;
final int height = pLayerInfo.bottom - pLayerInfo.top;
// private BufferedImage readLayerData(int layerIndex, final PSDLayerInfo pLayerInfo, final ImageTypeSpecifier pRawType, final ImageTypeSpecifier pImageType, ImageReadParam param) throws IOException {
private BufferedImage readLayerData(final int layerIndex, final ImageReadParam param) throws IOException {
final int width = getLayerWidth(layerIndex);
final int height = getLayerHeight(layerIndex);
PSDLayerInfo layerInfo = metadata.layerInfo.get(layerIndex);
// final int width = layerInfo.right - layerInfo.left;
// final int height = layerInfo.bottom - layerInfo.top;
// Even if raw/imageType has no alpha, the layers may still have alpha...
ImageTypeSpecifier imageType = getImageTypeForLayer(pImageType, pLayerInfo);
ImageTypeSpecifier imageType = getRawImageTypeForLayer(layerIndex);
// TODO: Find a better way of handling layers of size 0
// - Return null? Return a BufferedImage subclass that has no data (0 x 0)?
// Create image (or dummy, if h/w are <= 0)
BufferedImage layer = imageType.createBufferedImage(Math.max(1, width), Math.max(1, height));
// BufferedImage layer = imageType.createBufferedImage(Math.max(1, width), Math.max(1, height));
if (width <= 0 || height <= 0) {
return null;
}
BufferedImage layer = getDestination(param, getImageTypes(layerIndex + 1), Math.max(1, width), Math.max(1, height));
imageInput.seek(findLayerStartPos(layerIndex));
// Source/destination area
Rectangle area = new Rectangle(width, height);
@ -856,14 +942,14 @@ public class PSDImageReader extends ImageReaderBase {
final ColorModel destCM = layer.getColorModel();
// TODO: This raster is 3-5 times longer than needed, depending on number of channels...
ColorModel sourceCM = pRawType.getColorModel();
ColorModel sourceCM = imageType.getColorModel();
final WritableRaster rowRaster = width > 0 ? sourceCM.createCompatibleWritableRaster(width, 1) : null;
// final int channels = rowRaster.getNumBands();
final boolean banded = raster.getDataBuffer().getNumBanks() > 1;
final int interleavedBands = banded ? 1 : raster.getNumBands();
for (PSDChannelInfo channelInfo : pLayerInfo.channelInfo) {
for (PSDChannelInfo channelInfo : layerInfo.channelInfo) {
int compression = imageInput.readUnsignedShort();
// Skip layer if we can't read it
@ -875,7 +961,7 @@ public class PSDImageReader extends ImageReaderBase {
else {
// 0 = red, 1 = green, etc
// -1 = transparency mask; -2 = user supplied layer mask
int c = channelInfo.channelId == -1 ? pLayerInfo.channelInfo.length - 1 : channelInfo.channelId;
int c = channelInfo.channelId == -1 ? layerInfo.channelInfo.length - 1 : channelInfo.channelId;
// NOTE: For layers, byte counts are written per channel, while for the composite data
// byte counts are written for all channels before the image data.
@ -890,7 +976,7 @@ public class PSDImageReader extends ImageReaderBase {
// If RLE, the the image data starts with the byte counts
// for all the scan lines in the channel (LayerBottom-LayerTop), with
// each count stored as a two*byte value.
byteCounts = new int[pLayerInfo.bottom - pLayerInfo.top];
byteCounts = new int[layerInfo.bottom - layerInfo.top];
for (int i = 0; i < byteCounts.length; i++) {
byteCounts[i] = imageInput.readUnsignedShort();
}
@ -908,24 +994,14 @@ public class PSDImageReader extends ImageReaderBase {
switch (header.bits) {
case 1:
byte[] row1 = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
// DataBufferByte buffer1 = (DataBufferByte) raster.getDataBuffer();
// byte[] data1 = banded ? buffer1.getData(c) : buffer1.getData();
read1bitChannel(c, imageType.getNumBands(), raster.getDataBuffer(), interleavedBands, bandOffset, sourceCM, row1, area, area, xsub, ysub, width, height, byteCounts, compression == PSD.COMPRESSION_RLE);
break;
case 8:
byte[] row8 = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
// DataBufferByte buffer8 = (DataBufferByte) raster.getDataBuffer();
// byte[] data8 = banded ? buffer8.getData(c) : buffer8.getData();
// read8bitChannel(c, imageType.getNumBands(), data8, interleavedBands, bandOffset, sourceCM, row8, area, area, xsub, ysub, width, height, byteCounts, 0, compression == PSD.COMPRESSION_RLE);
read8bitChannel(c, imageType.getNumBands(), raster.getDataBuffer(), interleavedBands, bandOffset, sourceCM, row8, area, area, xsub, ysub, width, height, byteCounts, 0, compression == PSD.COMPRESSION_RLE);
break;
case 16:
short[] row16 = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
// DataBufferUShort buffer16 = (DataBufferUShort) raster.getDataBuffer();
// short[] data16 = banded ? buffer16.getData(c) : buffer16.getData();
read16bitChannel(c, imageType.getNumBands(), raster.getDataBuffer(), interleavedBands, bandOffset, sourceCM, row16, area, area, xsub, ysub, width, height, byteCounts, 0, compression == PSD.COMPRESSION_RLE);
break;
default:
@ -941,49 +1017,64 @@ public class PSDImageReader extends ImageReaderBase {
return layer;
}
private ImageTypeSpecifier getImageTypeForLayer(final ImageTypeSpecifier pOriginal, final PSDLayerInfo pLayerInfo) {
private ImageTypeSpecifier getRawImageTypeForLayer(final int layerIndex) throws IOException {
ImageTypeSpecifier compositeType = getRawImageTypeForCompositeLayer();
PSDLayerInfo layerInfo = metadata.layerInfo.get(layerIndex);
// If layer has more channels than composite data, it's normally extra alpha...
if (pLayerInfo.channelInfo.length > pOriginal.getNumBands()) {
if (layerInfo.channelInfo.length > compositeType.getNumBands()) {
// ...but, it could also be just the user mask...
boolean userMask = false;
for (PSDChannelInfo channelInfo : pLayerInfo.channelInfo) {
for (PSDChannelInfo channelInfo : layerInfo.channelInfo) {
if (channelInfo.channelId == -2) {
userMask = true;
break;
}
}
int newBandNum = pLayerInfo.channelInfo.length - (userMask ? 1 : 0);
int newBandNum = layerInfo.channelInfo.length - (userMask ? 1 : 0);
// If there really is more channels, then create new imageTypeSpec
if (newBandNum > pOriginal.getNumBands()) {
int[] offs = new int[newBandNum];
for (int i = 0, offsLength = offs.length; i < offsLength; i++) {
offs[i] = offsLength - i;
if (newBandNum > compositeType.getNumBands()) {
int[] indices = new int[newBandNum];
for (int i = 0, indicesLength = indices.length; i < indicesLength; i++) {
indices[i] = indicesLength - i;
}
return ImageTypeSpecifier.createInterleaved(pOriginal.getColorModel().getColorSpace(), offs, pOriginal.getSampleModel().getDataType(), true, false);
int[] offs = new int[newBandNum];
for (int i = 0, offsLength = offs.length; i < offsLength; i++) {
offs[i] = 0;
}
return ImageTypeSpecifier.createBanded(compositeType.getColorModel().getColorSpace(), indices, offs, compositeType.getSampleModel().getDataType(), true, false);
}
}
return pOriginal;
return compositeType;
}
/// Layer support
@Override
public int getNumImages(boolean allowSearch) throws IOException {
// NOTE: Spec says this method should throw IllegalStateException if allowSearch && isSeekForwardOnly()
// But that makes no sense for a format (like PSD) that does not need to search, right?
readHeader();
readImageResources(false);
readLayerAndMaskInfo(true); // TODO: Consider quicker reading of just the number of layers.
return metadata.layerInfo != null ? metadata.layerInfo.size() + 1 : 1; // TODO: Only plus one, if "has real merged data"?
}
// 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;
Point getOffset(int pImageIndex) throws IOException;
// Return 0, 0 for index 0, otherwise use layer offset
*/
@ -1087,26 +1178,29 @@ public class PSDImageReader extends ImageReaderBase {
// TODO: Thumbnail progress listeners...
PSDThumbnail thumbnail = getThumbnailResource(imageIndex, thumbnailIndex);
// TODO: Defer decoding
// TODO: It's possible to attach listeners to the ImageIO reader delegate... But do we really care?
processThumbnailStarted(imageIndex, thumbnailIndex);
processThumbnailProgress(0);
BufferedImage image = thumbnail.getThumbnail();
processThumbnailProgress(100);
processThumbnailComplete();
// TODO: Returning a cached mutable thumbnail is not really safe...
return thumbnail.getThumbnail();
return image;
}
/// Functional testing
public static void main(final String[] pArgs) throws IOException {
int subsampleFactor = 1;
Rectangle sourceRegion = null;
boolean readLayers = false;
boolean readThumbnails = false;
int idx = 0;
while (pArgs[idx].charAt(0) == '-') {
if (pArgs[idx].equals("-s")) {
if (pArgs[idx].equals("-s") || pArgs[idx].equals("--subsampling")) {
subsampleFactor = Integer.parseInt(pArgs[++idx]);
}
else if (pArgs[idx].equals("-r")) {
else if (pArgs[idx].equals("-r") || pArgs[idx].equals("--sourceregion")) {
int xw = Integer.parseInt(pArgs[++idx]);
int yh = Integer.parseInt(pArgs[++idx]);
@ -1126,6 +1220,12 @@ public class PSDImageReader extends ImageReaderBase {
System.out.println("sourceRegion: " + sourceRegion);
}
else if (pArgs[idx].equals("-l") || pArgs[idx].equals("--layers")) {
readLayers = true;
}
else if (pArgs[idx].equals("-t") || pArgs[idx].equals("--thumbnails")) {
readThumbnails = true;
}
else {
System.err.println("Usage: java PSDImageReader [-s <subsample factor>] [-r [<x y>] <w h>] <image file>");
System.exit(1);
@ -1158,7 +1258,6 @@ public class PSDImageReader extends ImageReaderBase {
node = metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
serializer = new XMLSerializer(System.out, System.getProperty("file.encoding"));
serializer.setIndentation(" ");
serializer.serialize(node, true);
System.out.println();
@ -1166,7 +1265,7 @@ public class PSDImageReader extends ImageReaderBase {
// serializer = new XMLSerializer(System.out, System.getProperty("file.encoding"));
serializer.serialize(node, true);
if (imageReader.hasThumbnails(0)) {
if (readThumbnails && imageReader.hasThumbnails(0)) {
int thumbnails = imageReader.getNumThumbnails(0);
for (int i = 0; i < thumbnails; i++) {
showIt(imageReader.readThumbnail(0, i), String.format("Thumbnail %d", i));
@ -1188,22 +1287,34 @@ public class PSDImageReader extends ImageReaderBase {
// param.setDestinationType(imageReader.getRawImageType(0));
BufferedImage image = imageReader.read(0, param);
System.out.println("time: " + (System.currentTimeMillis() - start));
System.out.println("read time: " + (System.currentTimeMillis() - start));
System.out.println("image: " + image);
if (image.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_CMYK) {
try {
ColorConvertOp op = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_sRGB), null);
image = op.filter(image, new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_4BYTE_ABGR_PRE));
GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
image = op.filter(image, gc.createCompatibleImage(image.getWidth(), image.getHeight(), image.getTransparency()));
}
catch (Exception e) {
e.printStackTrace();
image = ImageUtil.accelerate(image);
}
System.out.println("time: " + (System.currentTimeMillis() - start));
System.out.println("conversion time: " + (System.currentTimeMillis() - start));
System.out.println("image: " + image);
}
showIt(image, file.getName());
if (readLayers) {
int images = imageReader.getNumImages(true);
for (int i = 1; i < images; i++) {
start = System.currentTimeMillis();
BufferedImage layer = imageReader.read(i);
System.out.println("layer read time: " + (System.currentTimeMillis() - start));
System.err.println("layer: " + layer);
showIt(layer, "layer " + i);
}
}
}
}

View File

@ -10,6 +10,7 @@ import org.w3c.dom.Node;
import javax.imageio.metadata.IIOMetadataNode;
import java.awt.image.IndexColorModel;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
@ -33,6 +34,7 @@ public final class PSDMetadata extends AbstractMetadata {
List<PSDImageResource> imageResources;
PSDGlobalLayerMask globalLayerMask;
List<PSDLayerInfo> layerInfo;
long layersStart;
static final String[] COLOR_MODES = {
"MONOCHROME", "GRAYSCALE", "INDEXED", "RGB", "CMYK", null, null, "MULTICHANNEL", "DUOTONE", "LAB"
@ -240,12 +242,18 @@ public final class PSDMetadata extends AbstractMetadata {
node.setAttribute("fileVersion", String.valueOf(information.fileVersion));
}
else if (imageResource instanceof PSDThumbnail) {
// TODO: Revise/rethink this...
PSDThumbnail thumbnail = (PSDThumbnail) imageResource;
try {
// 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());
node = new IIOMetadataNode("Thumbnail");
// TODO: Thumbnail attributes + access to data, to avoid JPEG re-compression problems
node.setUserObject(thumbnail.getThumbnail());
}
catch (IOException e) {
// TODO: Warning
continue;
}
}
else if (imageResource instanceof PSDIPTCData) {
// TODO: Revise/rethink this...
@ -662,7 +670,7 @@ public final class PSDMetadata extends AbstractMetadata {
}
else if (textResource instanceof PSDXMPData) {
// TODO: Parse XMP (heavy) ONLY if we don't have required fields from IPTC/EXIF?
// TODO: Use XMP IPTC/EXIF/TIFFF NativeDigest field to validate if the values are in sync...
// TODO: Use XMP IPTC/EXIF/TIFF NativeDigest field to validate if the values are in sync..?
PSDXMPData xmp = (PSDXMPData) textResource;
}
}

View File

@ -1,11 +1,12 @@
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
/**
@ -16,9 +17,11 @@ import java.io.IOException;
* @version $Id: PSDThumbnail.java,v 1.0 Jul 29, 2009 4:41:06 PM haraldk Exp$
*/
class PSDThumbnail extends PSDImageResource {
private BufferedImage thumbnail;
private int format;
private int width;
private int height;
private int widthBytes;
private byte[] data;
public PSDThumbnail(final short pId, final ImageInputStream pInput) throws IOException {
super(pId, pInput);
@ -37,25 +40,14 @@ class PSDThumbnail extends PSDImageResource {
*/
@Override
protected void readData(final ImageInputStream pInput) throws IOException {
// TODO: Support for RAW RGB (format == 0): Extract RAW reader from PICT RAW QuickTime decompressor
int format = pInput.readInt();
switch (format) {
case 0:
// RAW RGB
throw new IIOException("RAW RGB format thumbnail not supported yet");
case 1:
// JPEG
break;
default:
throw new IIOException(String.format("Unsupported thumbnail format (%s) in PSD document", format));
}
format = pInput.readInt();
width = pInput.readInt();
height = pInput.readInt();
// This data isn't really useful, unless we're dealing with raw bytes
int widthBytes = pInput.readInt();
int totalSize = pInput.readInt();
widthBytes = pInput.readInt();
int totalSize = pInput.readInt(); // Hmm.. Is this really useful at all?
// Consistency check
int sizeCompressed = pInput.readInt();
@ -70,9 +62,21 @@ class PSDThumbnail extends PSDImageResource {
// TODO: Warning/Exception
}
// TODO: Defer decoding until getThumbnail?
// TODO: Support BGR if id == RES_THUMBNAIL_PS4? Or is that already supported in the JPEG?
thumbnail = ImageIO.read(IIOUtil.createStreamAdapter(pInput, sizeCompressed));
data = new byte[sizeCompressed];
pInput.readFully(data);
}
BufferedImage imageFromRawData(int width, int height, int scanLine, byte[] data) {
DataBuffer buffer = new DataBufferByte(data, data.length);
WritableRaster raster = Raster.createInterleavedRaster(
buffer, width, height,
scanLine, 3,
new int[]{0, 1, 2},
null
);
ColorModel cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
return new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
}
public final int getWidth() {
@ -83,15 +87,40 @@ class PSDThumbnail extends PSDImageResource {
return height;
}
public final BufferedImage getThumbnail() {
return thumbnail;
public final BufferedImage getThumbnail() throws IOException {
switch (format) {
case 0:
// RAW RGB
return imageFromRawData(width, height, widthBytes, data.clone()); // Clone data, as image is mutable
case 1:
// JPEG
// TODO: Support BGR if id == RES_THUMBNAIL_PS4? Or is that already supported in the JPEG reader?
return ImageIO.read(new ByteArrayInputStream(data));
default:
throw new IIOException(String.format("Unsupported thumbnail format (%s) in PSD document", format));
}
}
@Override
public String toString() {
StringBuilder builder = toStringBuilder();
builder.append(", ").append(thumbnail);
builder.append(", format: ");
switch (format) {
case 0:
// RAW RGB
builder.append("RAW RGB");
break;
case 1:
// JPEG
builder.append("JPEG");
break;
default:
builder.append("Unknown");
break;
}
builder.append(", size: ").append(data != null ? data.length : -1);
builder.append("]");

View File

@ -2,12 +2,16 @@ package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import org.junit.Test;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.ImageReader;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.io.IOException;
@ -133,8 +137,10 @@ public class PSDImageReaderTestCase extends ImageReaderAbstractTestCase<PSDImage
imageReader.setInput(getTestData().get(0).getInputStream());
int numImages = imageReader.getNumImages(true);
try {
imageReader.getNumThumbnails(2);
imageReader.getNumThumbnails(numImages + 1);
fail("Expected IndexOutOfBoundsException");
}
catch (IndexOutOfBoundsException expected) {
@ -159,7 +165,7 @@ public class PSDImageReaderTestCase extends ImageReaderAbstractTestCase<PSDImage
}
try {
imageReader.readThumbnail(99, 42);
imageReader.readThumbnail(numImages + 99, 42);
fail("Expected IndexOutOfBoundsException");
}
catch (IndexOutOfBoundsException expected) {
@ -212,4 +218,119 @@ public class PSDImageReaderTestCase extends ImageReaderAbstractTestCase<PSDImage
assertEquals("started", sequnce.get(0));
assertEquals("complete", sequnce.get(1));
}
@Test
public void testReadLayers() throws IOException {
PSDImageReader imageReader = createReader();
imageReader.setInput(getTestData().get(3).getInputStream());
int numImages = imageReader.getNumImages(true);
assertEquals(3, numImages);
for (int i = 0; i < numImages; i++) {
BufferedImage image = imageReader.read(i);
assertNotNull(image);
// Make sure layers are correct size
assertEquals(image.getWidth(), imageReader.getWidth(i));
assertEquals(image.getHeight(), imageReader.getHeight(i));
}
}
@Test
public void testImageTypesLayers() throws IOException {
PSDImageReader imageReader = createReader();
imageReader.setInput(getTestData().get(3).getInputStream());
int numImages = imageReader.getNumImages(true);
for (int i = 0; i < numImages; i++) {
ImageTypeSpecifier rawType = imageReader.getRawImageType(i);
// System.err.println("rawType: " + rawType);
assertNotNull(rawType);
Iterator<ImageTypeSpecifier> types = imageReader.getImageTypes(i);
assertNotNull(types);
assertTrue(types.hasNext());
boolean found = false;
while (types.hasNext()) {
ImageTypeSpecifier type = types.next();
// System.err.println("type: " + type);
if (!found && (rawType == type || rawType.equals(type))) {
found = true;
}
}
assertTrue("RAW image type not in type iterator", found);
}
}
@Test
public void testReadLayersExplicitType() throws IOException {
PSDImageReader imageReader = createReader();
imageReader.setInput(getTestData().get(3).getInputStream());
int numImages = imageReader.getNumImages(true);
for (int i = 0; i < numImages; i++) {
Iterator<ImageTypeSpecifier> types = imageReader.getImageTypes(i);
while (types.hasNext()) {
ImageTypeSpecifier type = types.next();
ImageReadParam param = imageReader.getDefaultReadParam();
param.setDestinationType(type);
BufferedImage image = imageReader.read(i, param);
assertEquals(type.getBufferedImageType(), image.getType());
if (type.getBufferedImageType() == 0) {
// TODO: If type.getBIT == 0, test more
// Compatible color model
assertEquals(type.getNumComponents(), image.getColorModel().getNumComponents());
// Same color space
assertEquals(type.getColorModel().getColorSpace(), image.getColorModel().getColorSpace());
// Same number of samples
assertEquals(type.getNumBands(), image.getSampleModel().getNumBands());
// Same number of bits/sample
for (int j = 0; j < type.getNumBands(); j++) {
assertEquals(type.getBitsPerBand(j), image.getSampleModel().getSampleSize(j));
}
}
}
}
}
@Test
public void testReadLayersExplicitDestination() throws IOException {
PSDImageReader imageReader = createReader();
imageReader.setInput(getTestData().get(3).getInputStream());
int numImages = imageReader.getNumImages(true);
for (int i = 0; i < numImages; i++) {
Iterator<ImageTypeSpecifier> types = imageReader.getImageTypes(i);
int width = imageReader.getWidth(i);
int height = imageReader.getHeight(i);
while (types.hasNext()) {
ImageTypeSpecifier type = types.next();
ImageReadParam param = imageReader.getDefaultReadParam();
BufferedImage destination = type.createBufferedImage(width, height);
param.setDestination(destination);
BufferedImage image = imageReader.read(i, param);
assertSame(destination, image);
}
}
}
}