- Minor clean-up in CMYKColorSpace, PSDColorData and PSDHeader

- Implemented raw type specifier to resemble file storage format
- Implemented banded reading for raw type
This commit is contained in:
Harald Kuhr 2009-10-01 21:41:01 +02:00
parent a782bcfc3b
commit 370505b62f
4 changed files with 192 additions and 91 deletions

View File

@ -37,7 +37,7 @@ import java.awt.color.ColorSpace;
* @author last modified by $Author: haraldk$
* @version $Id: CMYKColorSpace.java,v 1.0 Apr 30, 2008 1:38:13 PM haraldk Exp$
*/
// TODO: Move to com.twlevemonkeys.image?
// TODO: Move to com.twelvemonkeys.image?
// TODO: Read a ICC CMYK profile from classpath resource (from ECI)? ISO coated?
final class CMYKColorSpace extends ColorSpace {

View File

@ -68,11 +68,13 @@ class PSDColorData {
int[] rgb = toInterleavedRGB(mColors);
mColorModel = new InverseColorMapIndexColorModel(8, rgb.length, rgb, 0, false, -1, DataBuffer.TYPE_BYTE);
}
return mColorModel;
}
private int[] toInterleavedRGB(byte[] pColors) {
int[] rgb = new int[pColors.length / 3];
for (int i = 0; i < rgb.length; i++) {
// Pack the non-interleaved samples into interleaved form
int r = pColors[ i] & 0xff;
@ -81,6 +83,7 @@ class PSDColorData {
rgb[i] = (r << 16) | (g << 8) | b;
}
return rgb;
}
}

View File

@ -68,11 +68,14 @@ class PSDHeader {
}
int version = pInput.readUnsignedShort();
if (version != 1) {
if (version == 2) {
throw new IIOException("Large Document Format (PSB) not supported yet.");
}
throw new IIOException("Unknown PSD version, expected 1 or 2: 0x" + Integer.toHexString(version));
switch (version) {
case 1:
break;
case 2:
throw new IIOException("Photoshop Large Document Format (PSB) not supported yet.");
default:
throw new IIOException(String.format("Unknown PSD version, expected 1 or 2: 0x%08x", version));
}
byte[] reserved = new byte[6];
@ -80,7 +83,7 @@ class PSDHeader {
mChannels = pInput.readShort();
mHeight = pInput.readInt(); // Rows
mWidth = pInput.readInt(); // Coloumns
mWidth = pInput.readInt(); // Columns
mBits = pInput.readShort();
mMode = pInput.readShort();
}

View File

@ -32,10 +32,7 @@ import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.*;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
@ -59,7 +56,6 @@ import java.util.List;
* @author last modified by $Author: haraldk$
* @version $Id: PSDImageReader.java,v 1.0 Apr 29, 2008 4:45:52 PM haraldk Exp$
*/
// TODO: Figure out why reading directly from a FileInputStream is so dead slow...
// TODO: Implement meta data reading
// TODO: Implement layer reading
// TODO: Allow reading separate (or some?) layers
@ -98,49 +94,48 @@ public class PSDImageReader extends ImageReaderBase {
return mHeader.mHeight;
}
public Iterator<ImageTypeSpecifier> getImageTypes(final int pIndex) throws IOException {
// TODO: Check out the custom ImageTypeIterator and ImageTypeProducer used in the Sun provided JPEGImageReader
// Could use similar concept to create lazily-created ImageTypeSpecifiers (util candidate, based on FilterIterator?)
@Override
public ImageTypeSpecifier getRawImageType(int pIndex) throws IOException {
checkBounds(pIndex);
readHeader();
ColorSpace cs;
List<ImageTypeSpecifier> types = new ArrayList<ImageTypeSpecifier>();
switch (mHeader.mMode) {
case PSD.COLOR_MODE_MONOCHROME:
if (mHeader.mChannels == 1 && mHeader.mBits == 1) {
types.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_BINARY));
return ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_BINARY);
}
else {
throw new IIOException("Unsupported channel count/bit depth for Monochrome PSD: " + mHeader.mChannels + " channels/" + mHeader.mBits + " bits");
}
break;
throw new IIOException(
String.format("Unsupported channel count/bit depth for Monochrome PSD: %d channels/%d bits", mHeader.mChannels, mHeader.mBits)
);
case PSD.COLOR_MODE_INDEXED:
// TODO: 16 bit indexed?!
if (mHeader.mChannels == 1 && mHeader.mBits == 8) {
types.add(IndexedImageTypeSpecifier.createFromIndexColorModel(mColorData.getIndexColorModel()));
return IndexedImageTypeSpecifier.createFromIndexColorModel(mColorData.getIndexColorModel());
}
else {
throw new IIOException("Unsupported channel count/bit depth for Indexed Color PSD: " + mHeader.mChannels + " channels/" + mHeader.mBits + " bits");
}
break;
throw new IIOException(
String.format("Unsupported channel count/bit depth for Indexed Color PSD: %d channels/%d bits", mHeader.mChannels, mHeader.mBits)
);
case PSD.COLOR_MODE_DUOTONE:
// NOTE: Duotone (whatever that is) should be treated as gray scale, so fall-through
// NOTE: Duotone (whatever that is) should be treated as gray scale
// Fall-through
case PSD.COLOR_MODE_GRAYSCALE:
if (mHeader.mChannels == 1 && mHeader.mBits == 8) {
types.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY));
return ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY);
}
else if (mHeader.mChannels == 1 && mHeader.mBits == 16) {
types.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_USHORT_GRAY));
return ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_USHORT_GRAY);
}
else {
throw new IIOException(
String.format("Unsupported channel count/bit depth for Gray Scale PSD: %s channels/%s bits", mHeader.mChannels, mHeader.mBits)
);
}
break;
throw new IIOException(
String.format("Unsupported channel count/bit depth for Gray Scale PSD: %d channels/%d bits", mHeader.mChannels, mHeader.mBits)
);
case PSD.COLOR_MODE_RGB:
cs = getEmbeddedColorSpace();
if (cs == null) {
@ -148,11 +143,70 @@ public class PSDImageReader extends ImageReaderBase {
}
if (mHeader.mChannels == 3 && mHeader.mBits == 8) {
// types.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR));
return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2}, new int[] {0, 0, 0}, DataBuffer.TYPE_BYTE, false, false);
}
else if (mHeader.mChannels >= 4 && mHeader.mBits == 8) {
return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_BYTE, true, false);
}
else if (mHeader.mChannels == 3 && mHeader.mBits == 16) {
return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2}, new int[] {0, 0, 0}, DataBuffer.TYPE_USHORT, false, false);
}
else if (mHeader.mChannels >= 4 && mHeader.mBits == 16) {
return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_USHORT, true, false);
}
throw new IIOException(
String.format("Unsupported channel count/bit depth for RGB PSD: %d channels/%d bits", mHeader.mChannels, mHeader.mBits)
);
case PSD.COLOR_MODE_CMYK:
cs = getEmbeddedColorSpace();
if (cs == null) {
cs = CMYKColorSpace.getInstance();
}
if (mHeader.mChannels == 4 && mHeader.mBits == 8) {
return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_BYTE, false, false);
}
else if (mHeader.mChannels == 5 && mHeader.mBits == 8) {
return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, DataBuffer.TYPE_BYTE, true, false);
}
else if (mHeader.mChannels == 4 && mHeader.mBits == 16) {
return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_USHORT, false, false);
}
else if (mHeader.mChannels == 5 && mHeader.mBits == 16) {
return ImageTypeSpecifier.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, DataBuffer.TYPE_USHORT, true, false);
}
throw new IIOException(
String.format("Unsupported channel count/bit depth for CMYK PSD: %d channels/%d bits", mHeader.mChannels, mHeader.mBits)
);
default:
throw new IIOException(
String.format("Unsupported PSD MODE: %s (%d channels/%d bits)", mHeader.mMode, mHeader.mChannels, mHeader.mBits)
);
}
}
public Iterator<ImageTypeSpecifier> getImageTypes(final int pIndex) throws IOException {
// TODO: Check out the custom ImageTypeIterator and ImageTypeProducer used in the Sun provided JPEGImageReader
// Could use similar concept to create lazily-created ImageTypeSpecifiers (util candidate, based on FilterIterator?)
// Get the raw type. Will fail for unsupported types
ImageTypeSpecifier rawType = getRawImageType(pIndex);
ColorSpace cs = rawType.getColorModel().getColorSpace();
List<ImageTypeSpecifier> types = new ArrayList<ImageTypeSpecifier>();
switch (mHeader.mMode) {
case PSD.COLOR_MODE_RGB:
// Prefer interleaved versions as they are much faster to display
if (mHeader.mChannels == 3 && mHeader.mBits == 8) {
// Basically same as BufferedImage.TYPE_3BYTE_BGR
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {2, 1, 0}, DataBuffer.TYPE_BYTE, false, false));
}
else if (mHeader.mChannels >= 4 && mHeader.mBits == 8) {
// types.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR));
// Basically same as BufferedImage.TYPE_4BYTE_ABGR
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, true, false));
}
else if (mHeader.mChannels == 3 && mHeader.mBits == 16) {
@ -161,44 +215,32 @@ public class PSDImageReader extends ImageReaderBase {
else if (mHeader.mChannels >= 4 && mHeader.mBits == 16) {
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_USHORT, true, false));
}
else {
throw new IIOException(
String.format("Unsupported channel count/bit depth for RGB PSD: %d channels/%d bits", mHeader.mChannels, mHeader.mBits)
);
}
break;
case PSD.COLOR_MODE_CMYK:
// Prefer interleaved versions as they are much faster to display
// TODO: We should convert these to their RGB equivalents while reading for the common-case,
// as Java2D is extremely slow displaying custom images.
// Converting to RGB is also correct behaviour, according to the docs.
// The code below is, however, correct for raw type.
cs = getEmbeddedColorSpace();
if (cs == null) {
cs = CMYKColorSpace.getInstance();
}
if (mHeader.mChannels == 4 && mHeader.mBits == 8) {
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[]{3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false));
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false));
}
else if (mHeader.mChannels == 5 && mHeader.mBits == 8) {
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[]{4, 3, 2, 1, 0}, DataBuffer.TYPE_BYTE, true, false));
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {4, 3, 2, 1, 0}, DataBuffer.TYPE_BYTE, true, false));
}
else if (mHeader.mChannels == 4 && mHeader.mBits == 16) {
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[]{3, 2, 1, 0}, DataBuffer.TYPE_USHORT, false, false));
}
else if (mHeader.mChannels == 5 && mHeader.mBits == 16) {
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[]{4, 3, 2, 1, 0}, DataBuffer.TYPE_USHORT, true, false));
}
else {
throw new IIOException(
String.format("Unsupported channel count/bit depth for CMYK PSD: %d channels/%d bits", mHeader.mChannels, mHeader.mBits)
);
types.add(ImageTypeSpecifier.createInterleaved(cs, new int[] {4, 3, 2, 1, 0}, DataBuffer.TYPE_USHORT, true, false));
}
break;
default:
throw new IIOException("Unsupported PSD MODE: " + mHeader.mMode);
// Just stick to the raw type
}
// Finally add the
types.add(rawType);
return types.iterator();
}
@ -222,7 +264,6 @@ public class PSDImageReader extends ImageReaderBase {
return mColorSpace;
}
// TODO: Implement param handling
public BufferedImage read(int pIndex, ImageReadParam pParam) throws IOException {
checkBounds(pIndex);
@ -249,6 +290,10 @@ public class PSDImageReader extends ImageReaderBase {
// Otherwise, copy "through" ColorMode?l
// Copy pixels from temp raster
// If possible, leave the destination image "untouched" (accelerated)
// TODO: Doing a per line color convert will be expensive, as data is channelled...
// Will need to either convert entire image, or skip back/forth between channels...
// TODO: Banding...
ImageTypeSpecifier spec = getRawImageType(pIndex);
@ -260,9 +305,6 @@ public class PSDImageReader extends ImageReaderBase {
*/
// TODO: Maybe a banded raster would be easier than interleaved? We could still convert to interleaved in CCOp
WritableRaster raster = image.getRaster();
final int xSub;
final int ySub;
@ -299,19 +341,18 @@ public class PSDImageReader extends ImageReaderBase {
switch (mHeader.mBits) {
case 1:
read1bitData(raster, image.getColorModel(), source, dest, xSub, ySub, offsets, compression == PSD.COMPRESSION_RLE);
read1bitData(image.getRaster(), image.getColorModel(), source, dest, xSub, ySub, offsets, compression == PSD.COMPRESSION_RLE);
break;
case 8:
read8bitData(raster, image.getColorModel(), source, dest, xSub, ySub, offsets, compression == PSD.COMPRESSION_RLE);
read8bitData(image.getRaster(), image.getColorModel(), source, dest, xSub, ySub, offsets, compression == PSD.COMPRESSION_RLE);
break;
case 16:
read16bitData(raster, image.getColorModel(), source, dest, xSub, ySub, offsets, compression == PSD.COMPRESSION_RLE);
read16bitData(image.getRaster(), image.getColorModel(), source, dest, xSub, ySub, offsets, compression == PSD.COMPRESSION_RLE);
break;
default:
throw new IIOException("Unknown bit depth: " + mHeader.mBits);
}
if (abortRequested()) {
processReadAborted();
}
@ -328,7 +369,6 @@ public class PSDImageReader extends ImageReaderBase {
final int[] pRowOffsets, final boolean pRLECompressed) throws IOException
{
final int channels = pRaster.getNumBands();
final short[] data = ((DataBufferUShort) pRaster.getDataBuffer()).getData();
// TODO: FixMe: Use real source color model from native (raw) image type, and convert if needed
ColorModel sourceColorModel = pDestinationColorModel;
@ -339,9 +379,15 @@ public class PSDImageReader extends ImageReaderBase {
final boolean isCMYK = sourceColorModel.getColorSpace().getType() == ColorSpace.TYPE_CMYK;
final int colorComponents = sourceColorModel.getColorSpace().getNumComponents();
DataBufferUShort buffer = (DataBufferUShort) pRaster.getDataBuffer();
final boolean banded = buffer.getNumBanks() > 1;
short[] data = null;
int x = 0, y = 0, c = 0;
try {
for (c = 0; c < channels; c++) {
data = banded ? buffer.getData(c) : buffer.getData();
for (y = 0; y < mHeader.mHeight; y++) {
// Length is in shorts!?
int length = 2 * (pRLECompressed ? pRowOffsets[c * mHeader.mHeight + y] : mHeader.mWidth);
@ -366,7 +412,9 @@ public class PSDImageReader extends ImageReaderBase {
// TODO: Destination offset...??
// Copy line sub sampled into real data
int offset = (y - pSource.y) / pYSub * pDest.width * channels + (channels - 1 - c);
int offset = banded ?
(y - pSource.y) / pYSub * pDest.width :
(y - pSource.y) / pYSub * pDest.width * channels + (channels - 1 - c);
for (int i = 0; i < pDest.width; i++) {
short value = row[pSource.x + i * pXSub];
@ -375,7 +423,12 @@ public class PSDImageReader extends ImageReaderBase {
value = (short) (65535 - value & 0xffff);
}
data[offset + i * channels] = value;
if (banded) {
data[offset + i] = value;
}
else {
data[offset + i * channels] = value;
}
}
}
else {
@ -419,7 +472,6 @@ public class PSDImageReader extends ImageReaderBase {
final int[] pRowOffsets, final boolean pRLECompressed) throws IOException
{
final int channels = pRaster.getNumBands();
final byte[] data = ((DataBufferByte) pRaster.getDataBuffer()).getData();
// TODO: FixMe: Use real source color model from native (raw) image type, and convert if needed
ColorModel sourceColorModel = pDestinationColorModel;
@ -430,9 +482,15 @@ public class PSDImageReader extends ImageReaderBase {
final boolean isCMYK = sourceColorModel.getColorSpace().getType() == ColorSpace.TYPE_CMYK;
final int colorComponents = sourceColorModel.getColorSpace().getNumComponents();
DataBufferByte buffer = (DataBufferByte) pRaster.getDataBuffer();
final boolean banded = buffer.getNumBanks() > 1;
byte[] data = null;
int x = 0, y = 0, c = 0;
try {
for (c = 0; c < channels; c++) {
data = banded ? buffer.getData(c) : buffer.getData();
for (y = 0; y < mHeader.mHeight; y++) {
int length = pRLECompressed ? pRowOffsets[c * mHeader.mHeight + y] : mHeader.mWidth;
@ -452,9 +510,13 @@ public class PSDImageReader extends ImageReaderBase {
mImageInput.readFully(row, 0, mHeader.mWidth);
}
// 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 * channels + (channels - 1 - c);
// int offset = (y - pSource.y) / pYSub * pDest.width * channels + (channels - 1 - c);
int offset = banded ?
(y - pSource.y) / pYSub * pDest.width :
(y - pSource.y) / pYSub * pDest.width * channels + (channels - 1 - c);
for (int i = 0; i < pDest.width; i++) {
byte value = row[pSource.x + i * pXSub];
@ -463,7 +525,12 @@ public class PSDImageReader extends ImageReaderBase {
value = (byte) (255 - value & 0xff);
}
data[offset + i * channels] = value;
if (banded) {
data[offset + i] = value;
}
else {
data[offset + i * channels] = value;
}
}
}
else {
@ -497,7 +564,7 @@ public class PSDImageReader extends ImageReaderBase {
}
// Compose out the background of the semi-transparent pixels, as PS somehow has the background composed in
decomposeAlpha(sourceColorModel, data, pDest.width, pDest.height, channels);
decomposeAlpha(sourceColorModel, buffer, pDest.width, pDest.height, channels);
}
private void read1bitData(final WritableRaster pRaster, final ColorModel pDestinationColorModel,
@ -591,29 +658,57 @@ public class PSDImageReader extends ImageReaderBase {
}
}
private void decomposeAlpha(final ColorModel pModel, final byte[] pData,
final int pWidth, final int pHeight, final int pChannels) throws IOException
{
private void decomposeAlpha(final ColorModel pModel, final DataBufferByte pBuffer,
final int pWidth, final int pHeight, final int pChannels) {
// TODO: Is the document background always white!?
// TODO: What about CMYK + alpha?
if (pModel.hasAlpha() && pModel.getColorSpace().getType() == ColorSpace.TYPE_RGB) {
// TODO: Probably faster to do this in line..
for (int y = 0; y < pHeight; y++) {
for (int x = 0; x < pWidth; x++) {
int offset = (x + y * pWidth) * pChannels;
// TODO: Is the document background always white!?
// ABGR format
int alpha = pData[offset] & 0xff;
if (alpha != 0) {
double normalizedAlpha = alpha / 255.0;
for (int i = 1; i < pChannels; i++) {
pData[offset + i] = decompose(pData[offset + i] & 0xff, normalizedAlpha);
// TODO: Probably faster to do this in line..
if (pBuffer.getNumBanks() > 1) {
byte[][] data = pBuffer.getBankData();
for (int y = 0; y < pHeight; y++) {
for (int x = 0; x < pWidth; x++) {
int offset = (x + y * pWidth);
// ARGB format
int alpha = data[pChannels - 1][offset] & 0xff;
if (alpha != 0) {
double normalizedAlpha = alpha / 255.0;
for (int i = 0; i < pChannels - 1; i++) {
data[i][offset] = decompose(data[i][offset] & 0xff, normalizedAlpha);
}
}
else {
for (int i = 0; i < pChannels - 1; i++) {
data[i][offset] = 0;
}
}
}
else {
for (int i = 1; i < pChannels; i++) {
pData[offset + i] = 0;
}
}
else {
byte[] data = pBuffer.getData();
for (int y = 0; y < pHeight; y++) {
for (int x = 0; x < pWidth; x++) {
int offset = (x + y * pWidth) * pChannels;
// ABGR format
int alpha = data[offset] & 0xff;
if (alpha != 0) {
double normalizedAlpha = alpha / 255.0;
for (int i = 1; i < pChannels; i++) {
data[offset + i] = decompose(data[offset + i] & 0xff, normalizedAlpha);
}
}
else {
for (int i = 1; i < pChannels; i++) {
data[offset + i] = 0;
}
}
}
}