#483 Initial PSD Write support

(cherry picked from commit 15a9ad0a9bab1b30f73439912275322c01af4e49)
This commit is contained in:
Harald Kuhr 2021-05-06 00:01:54 +02:00
parent 5661e7459c
commit 2930708a54
19 changed files with 957 additions and 132 deletions

View File

@ -0,0 +1,122 @@
package com.twelvemonkeys.imageio.util;
import java.awt.*;
import java.awt.image.*;
import static com.twelvemonkeys.lang.Validate.notNull;
/**
* A class containing various raster utility methods.
*/
public final class RasterUtils {
private RasterUtils() {
}
/**
* Returns a raster with {@code DataBuffer.TYPE_BYTE} transfer type.
* Works for any raster from a {@code BufferedImage.TYPE_INT_*} image
*
* @param raster a {@code Raster} with either transfer type {@code DataBuffer.TYPE_BYTE}
* or {@code DataBuffer.TYPE_INT} with `SinglePixelPackedSampleModel`, not {@code null}.
* @return a raster with {@code DataBuffer.TYPE_BYTE} transfer type.
* @throws IllegalArgumentException if {@code raster} does not have transfer type {@code DataBuffer.TYPE_BYTE}
* or {@code DataBuffer.TYPE_INT} with `SinglePixelPackedSampleModel`
* @throws NullPointerException if {@code raster} is {@code null}.
*/
public static Raster asByteRaster(final Raster raster) {
return asByteRaster0(raster);
}
/**
* Returns a writable raster with {@code DataBuffer.TYPE_BYTE} transfer type.
* Works for any raster from a {@code BufferedImage.TYPE_INT_*} image.
*
* @param raster a {@code WritableRaster} with either transfer type {@code DataBuffer.TYPE_BYTE}
* or {@code DataBuffer.TYPE_INT} with `SinglePixelPackedSampleModel`, not {@code null}.
* @return a writable raster with {@code DataBuffer.TYPE_BYTE} transfer type.
* @throws IllegalArgumentException if {@code raster} does not have transfer type {@code DataBuffer.TYPE_BYTE}
* or {@code DataBuffer.TYPE_INT} with `SinglePixelPackedSampleModel`
* @throws NullPointerException if {@code raster} is {@code null}.
*/
public static WritableRaster asByteRaster(final WritableRaster raster) {
return (WritableRaster) asByteRaster0(raster);
}
private static Raster asByteRaster0(final Raster raster) {
switch (raster.getTransferType()) {
case DataBuffer.TYPE_BYTE:
return raster;
case DataBuffer.TYPE_INT:
SampleModel sampleModel = raster.getSampleModel();
if (!(sampleModel instanceof SinglePixelPackedSampleModel)) {
throw new IllegalArgumentException(String.format("Requires SinglePixelPackedSampleModel, %s not supported", sampleModel.getClass().getSimpleName()));
}
final int bands = 4;
final DataBufferInt buffer = (DataBufferInt) raster.getDataBuffer();
int w = raster.getWidth();
int h = raster.getHeight();
int size = buffer.getSize();
return new WritableRaster(
new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, w, h, bands, w * bands, createBandOffsets((SinglePixelPackedSampleModel) sampleModel)),
new DataBuffer(DataBuffer.TYPE_BYTE, size * bands) {
final int[] MASKS = {
0xffffff00,
0xffff00ff,
0xff00ffff,
0x00ffffff,
};
@Override
public int getElem(int bank, int i) {
int index = i / bands;
int shift = (i % bands) * 8;
return (buffer.getElem(index) >>> shift) & 0xff;
}
@Override
public void setElem(int bank, int i, int val) {
int index = i / bands;
int element = i % bands;
int shift = element * 8;
int value = (buffer.getElem(index) & MASKS[element]) | ((val & 0xff) << shift);
buffer.setElem(index, value);
}
}, new Point()) {
};
default:
throw new IllegalArgumentException(String.format("Raster type %d not supported", raster.getTransferType()));
}
}
private static int[] createBandOffsets(final SinglePixelPackedSampleModel sampleModel) {
notNull(sampleModel, "sampleModel");
int[] masks = sampleModel.getBitMasks();
int[] offs = new int[masks.length];
for (int i = 0; i < masks.length; i++) {
int mask = masks[i];
int off = 0;
// TODO: FixMe! This only works for standard 8 bit masks (0xFF)
if (mask != 0) {
while ((mask & 0xFF) == 0) {
mask >>>= 8;
off++;
}
}
offs[i] = off;
}
return offs;
}
}

View File

@ -0,0 +1,199 @@
package com.twelvemonkeys.imageio.util;
import org.junit.Test;
import javax.imageio.ImageTypeSpecifier;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.util.Random;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
/**
* RasterUtilsTest.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: RasterUtilsTest.java,v 1.0 05/05/2021 haraldk Exp$
*/
public class RasterUtilsTest {
@Test(expected = NullPointerException.class)
public void testAsByteRasterFromNull() {
RasterUtils.asByteRaster((Raster) null);
}
@SuppressWarnings("RedundantCast")
@Test(expected = NullPointerException.class)
public void testAsByteRasterWritableFromNull() {
RasterUtils.asByteRaster((WritableRaster) null);
}
@Test
public void testAsByteRasterPassThrough() {
WritableRaster[] rasters = new WritableRaster[] {
new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR).getRaster(),
new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR).getRaster(),
new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR_PRE).getRaster(),
new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY).getRaster(),
Raster.createBandedRaster(DataBuffer.TYPE_BYTE, 1, 1, 7, null),
Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 2, null),
new WritableRaster(new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, 1, 1, 1, 1, new int[1]), new Point(0, 0)) {}
};
for (Raster raster : rasters) {
assertSame(raster, RasterUtils.asByteRaster(raster));
}
for (WritableRaster raster : rasters) {
assertSame(raster, RasterUtils.asByteRaster(raster));
}
}
@Test
public void testAsByteRasterWritableFromTYPE_INT_RGB() {
BufferedImage image = new BufferedImage(9, 11, BufferedImage.TYPE_INT_RGB);
WritableRaster raster = RasterUtils.asByteRaster(image.getRaster());
assertEquals(DataBuffer.TYPE_BYTE, raster.getTransferType());
assertEquals(PixelInterleavedSampleModel.class, raster.getSampleModel().getClass());
assertEquals(image.getWidth(), raster.getWidth());
assertEquals(image.getHeight(), raster.getHeight());
assertEquals(3, raster.getNumBands());
assertEquals(3, raster.getNumDataElements());
assertImageRasterEquals(image, raster);
}
@Test
public void testAsByteRasterWritableFromTYPE_INT_ARGB() {
BufferedImage image = new BufferedImage(9, 11, BufferedImage.TYPE_INT_ARGB);
WritableRaster raster = RasterUtils.asByteRaster(image.getRaster());
assertEquals(DataBuffer.TYPE_BYTE, raster.getTransferType());
assertEquals(PixelInterleavedSampleModel.class, raster.getSampleModel().getClass());
assertEquals(image.getWidth(), raster.getWidth());
assertEquals(image.getHeight(), raster.getHeight());
assertEquals(4, raster.getNumBands());
assertEquals(4, raster.getNumDataElements());
assertImageRasterEquals(image, raster);
}
@Test
public void testAsByteRasterWritableFromTYPE_INT_ARGB_PRE() {
BufferedImage image = new BufferedImage(9, 11, BufferedImage.TYPE_INT_ARGB_PRE);
WritableRaster raster = RasterUtils.asByteRaster(image.getRaster());
assertEquals(DataBuffer.TYPE_BYTE, raster.getTransferType());
assertEquals(PixelInterleavedSampleModel.class, raster.getSampleModel().getClass());
assertEquals(image.getWidth(), raster.getWidth());
assertEquals(image.getHeight(), raster.getHeight());
assertEquals(4, raster.getNumBands());
assertEquals(4, raster.getNumDataElements());
// We don't assert on values here, as the premultiplied values makes it hard...
}
@Test
public void testAsByteRasterWritableFromTYPE_INT_BGR() {
BufferedImage image = new BufferedImage(9, 11, BufferedImage.TYPE_INT_BGR);
WritableRaster raster = RasterUtils.asByteRaster(image.getRaster());
assertEquals(DataBuffer.TYPE_BYTE, raster.getTransferType());
assertEquals(PixelInterleavedSampleModel.class, raster.getSampleModel().getClass());
assertEquals(image.getWidth(), raster.getWidth());
assertEquals(image.getHeight(), raster.getHeight());
assertEquals(3, raster.getNumBands());
assertEquals(3, raster.getNumDataElements());
assertImageRasterEquals(image, raster);
}
@Test
public void testAsByteRasterWritableFromTYPE_CUSTOM_GRAB() {
BufferedImage image = ImageTypeSpecifier.createPacked(ColorSpace.getInstance(ColorSpace.CS_sRGB),
0x00FF0000,
0xFF000000,
0x000000FF,
0x0000FF00,
DataBuffer.TYPE_INT, false).createBufferedImage(7, 13);
WritableRaster raster = RasterUtils.asByteRaster(image.getRaster());
assertEquals(DataBuffer.TYPE_BYTE, raster.getTransferType());
assertEquals(PixelInterleavedSampleModel.class, raster.getSampleModel().getClass());
assertEquals(image.getWidth(), raster.getWidth());
assertEquals(image.getHeight(), raster.getHeight());
assertEquals(4, raster.getNumBands());
assertEquals(4, raster.getNumDataElements());
assertImageRasterEquals(image, raster);
}
@Test
public void testAsByteRasterWritableFromTYPE_CUSTOM_BxRG() {
BufferedImage image = ImageTypeSpecifier.createPacked(ColorSpace.getInstance(ColorSpace.CS_sRGB),
0x0000FF00,
0x000000FF,
0xFF000000,
0,
DataBuffer.TYPE_INT, false).createBufferedImage(7, 13);
WritableRaster raster = RasterUtils.asByteRaster(image.getRaster());
assertEquals(DataBuffer.TYPE_BYTE, raster.getTransferType());
assertEquals(PixelInterleavedSampleModel.class, raster.getSampleModel().getClass());
assertEquals(image.getWidth(), raster.getWidth());
assertEquals(image.getHeight(), raster.getHeight());
assertEquals(3, raster.getNumBands());
assertEquals(3, raster.getNumDataElements());
assertImageRasterEquals(image, raster);
}
private static void assertImageRasterEquals(BufferedImage image, WritableRaster raster) {
// NOTE: This is NOT necessarily how the values are stored in the data buffer
int[] argbOffs = new int[] {16, 8, 0, 24};
Raster imageRaster = image.getRaster();
Random rng = new Random(27365481723L);
for (int y = 0; y < raster.getHeight(); y++) {
for (int x = 0; x < raster.getWidth(); x++) {
int argb = 0;
for (int b = 0; b < raster.getNumBands(); b++) {
int s = rng.nextInt(0xFF);
raster.setSample(x, y, b, s);
assertEquals(s, raster.getSample(x, y, b));
assertEquals(s, imageRaster.getSample(x, y, b));
argb |= (s << argbOffs[b]);
}
if (raster.getNumBands() < 4) {
argb |= 0xFF000000;
}
int expectedArgb = image.getRGB(x, y);
if (argb != expectedArgb) {
assertEquals(x + ", " + y + ": ", String.format("#%08x", expectedArgb), String.format("#%08x", argb));
}
}
}
}
}

View File

@ -60,7 +60,7 @@ public final class TIFFWriter extends MetadataWriter {
private static final int LONGWORD_LENGTH = 4;
private static final int ENTRY_LENGTH = 12;
public boolean write(final Collection<Entry> entries, final ImageOutputStream stream) throws IOException {
public boolean write(final Collection<? extends Entry> entries, final ImageOutputStream stream) throws IOException {
return write(new IFD(entries), stream);
}

View File

@ -33,6 +33,7 @@ package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.util.IIOUtil;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import java.awt.color.ICC_Profile;
import java.io.IOException;
import java.io.InputStream;
@ -52,14 +53,23 @@ final class ICCProfile extends PSDImageResource {
}
@Override
protected void readData(ImageInputStream pInput) throws IOException {
InputStream stream = IIOUtil.createStreamAdapter(pInput, size);
try {
protected void readData(final ImageInputStream pInput) throws IOException {
try (InputStream stream = IIOUtil.createStreamAdapter(pInput, size)) {
profile = ICC_Profile.getInstance(stream);
}
finally {
// Make sure stream has correct position after read
stream.close();
}
static void writeData(final ImageOutputStream output, final ICC_Profile profile) throws IOException {
output.writeInt(PSD.RESOURCE_TYPE);
output.writeShort(PSD.RES_ICC_PROFILE);
output.writeShort(0); // Zero-length Pascal name + pad
byte[] data = profile.getData();
output.writeInt(data.length + data.length % 2);
output.write(data);
if (data.length % 2 != 0) {
output.write(0); // pad
}
}

View File

@ -31,11 +31,16 @@
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFWriter;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.stream.SubImageOutputStream;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import java.io.IOException;
import java.util.Collection;
/**
* EXIF metadata.
@ -60,6 +65,27 @@ final class PSDEXIF1Data extends PSDDirectoryResource {
return new TIFFReader().read(new ByteArrayImageInputStream(data));
}
static void writeData(final ImageOutputStream output, final Collection<? extends Entry> directory) throws IOException {
output.writeInt(PSD.RESOURCE_TYPE);
output.writeShort(PSD.RES_EXIF_DATA_1);
output.writeShort(0); // Zero-length Pascal name + pad
output.writeInt(0); // Dummy length
long beforeExif = output.getStreamPosition();
new TIFFWriter().write(directory, new SubImageOutputStream(output));
long afterExif = output.getStreamPosition();
if ((afterExif - beforeExif) % 2 != 0) {
afterExif++;
output.write(0); // Pad
}
// Update length
output.seek(beforeExif - 4);
output.writeInt((int) (afterExif - beforeExif));
output.seek(afterExif);
}
@Override
public String toString() {
Directory directory = getDirectory();

View File

@ -31,6 +31,7 @@
package com.twelvemonkeys.imageio.plugins.psd;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageOutputStream;
import java.io.DataInput;
import java.io.IOException;
@ -57,8 +58,8 @@ final class PSDHeader {
// WORD Mode; /* Color mode */
// } PSD_HEADER;
private static final int PSD_MAX_SIZE = 30000;
private static final int PSB_MAX_SIZE = 300000;
static final int PSD_MAX_SIZE = 30000;
static final int PSB_MAX_SIZE = 300000;
final short channels;
final int width;
@ -67,7 +68,57 @@ final class PSDHeader {
final short mode;
final boolean largeFormat;
PSDHeader(final DataInput pInput) throws IOException {
PSDHeader(int channels, int width, int height, int bits, int mode, boolean largeFormat) {
this((short) channels, width, height, (short) bits, (short) mode, largeFormat);
}
private PSDHeader(short channels, int width, int height, short bits, short mode, boolean largeFormat) {
if (channels < 1 || channels > 56) {
throw new IllegalArgumentException(String.format("Unsupported number of channels for PSD: %d", channels));
}
this.channels = channels;
this.width = width;
this.height = height;
switch (bits) {
case 1:
case 8:
case 16:
case 32:
break;
default:
throw new IllegalArgumentException(String.format("Unsupported bit depth for PSD: %d bits", bits));
}
this.bits = bits;
switch (mode) {
case PSD.COLOR_MODE_BITMAP:
case PSD.COLOR_MODE_GRAYSCALE:
case PSD.COLOR_MODE_INDEXED:
case PSD.COLOR_MODE_RGB:
case PSD.COLOR_MODE_CMYK:
case PSD.COLOR_MODE_MULTICHANNEL:
case PSD.COLOR_MODE_DUOTONE:
case PSD.COLOR_MODE_LAB:
break;
default:
throw new IllegalArgumentException(String.format("Unsupported color mode for PSD: %d", mode));
}
this.mode = mode;
this.largeFormat = largeFormat;
if (!hasValidDimensions()) {
throw new IllegalArgumentException(String.format("Dimensions exceed maximum allowed for %s: %dx%d (max %dx%d)",
largeFormat ? "PSB" : "PSD",
width, height, getMaxSize(), getMaxSize()));
}
}
static PSDHeader read(final DataInput pInput) throws IOException {
int signature = pInput.readInt();
if (signature != PSD.SIGNATURE_8BPS) {
throw new IIOException("Not a PSD document, expected signature \"8BPS\": \"" + PSDUtil.intToStr(signature) + "\" (0x" + Integer.toHexString(signature) + ")");
@ -75,6 +126,7 @@ final class PSDHeader {
int version = pInput.readUnsignedShort();
boolean largeFormat;
switch (version) {
case PSD.VERSION_PSD:
largeFormat = false;
@ -89,15 +141,15 @@ final class PSDHeader {
byte[] reserved = new byte[6];
pInput.readFully(reserved); // We don't really care
channels = pInput.readShort();
short channels = pInput.readShort();
if (channels < 1 || channels > 56) {
throw new IIOException(String.format("Unsupported number of channels for PSD: %d", channels));
}
height = pInput.readInt(); // Rows
width = pInput.readInt(); // Columns
int height = pInput.readInt(); // Rows
int width = pInput.readInt(); // Columns
bits = pInput.readShort();
short bits = pInput.readShort();
switch (bits) {
case 1:
@ -109,7 +161,7 @@ final class PSDHeader {
throw new IIOException(String.format("Unsupported bit depth for PSD: %d bits", bits));
}
mode = pInput.readShort();
short mode = pInput.readShort();
switch (mode) {
case PSD.COLOR_MODE_BITMAP:
@ -124,6 +176,21 @@ final class PSDHeader {
default:
throw new IIOException(String.format("Unsupported color mode for PSD: %d", mode));
}
return new PSDHeader(channels, width, height, bits, mode, largeFormat);
}
void write(ImageOutputStream output) throws IOException {
output.writeInt(PSD.SIGNATURE_8BPS);
output.writeShort(largeFormat ? PSD.VERSION_PSB : PSD.VERSION_PSD);
output.write(new byte[6]); // Reserved
output.writeShort(channels);
output.writeInt(height); // Columns
output.writeInt(width); // Rows
output.writeShort(bits);
output.writeShort(mode);
}
@Override
@ -177,4 +244,5 @@ final class PSDHeader {
return "Unkown mode";
}
}
}

View File

@ -842,7 +842,7 @@ public final class PSDImageReader extends ImageReaderBase {
assertInput();
if (header == null) {
header = new PSDHeader(imageInput);
header = PSDHeader.read(imageInput);
if (!header.hasValidDimensions()) {
processWarningOccurred(String.format("Dimensions exceed maximum allowed for %s: %dx%d (max %dx%d)",
@ -930,13 +930,12 @@ public final class PSDImageReader extends ImageReaderBase {
// NOTE: The spec says that if this section is empty, the length should be 0.
// Yet I have a PSB file that has size 12, and both contained lengths set to 0 (which
// is alo not as per spec, as layer count should be included if there's a layer info
// is also not as per spec, as layer count should be included if there's a layer info
// block, so minimum size should be either 0 or 14 (or 16 if multiple of 4 for PSB))...
if (layerAndMaskInfoLength > 0) {
long pos = imageInput.getStreamPosition();
//if (metadata.layerInfo == null) {
long layerInfoLength = header.largeFormat ? imageInput.readLong() : imageInput.readUnsignedInt();
if (layerInfoLength > 0) {
@ -991,7 +990,6 @@ public final class PSDImageReader extends ImageReaderBase {
System.out.println("layerInfo: " + metadata.layerInfo);
System.out.println("globalLayerMask: " + (metadata.globalLayerMask != PSDGlobalLayerMask.NULL_MASK ? metadata.globalLayerMask : null));
}
//}
}
metadata.imageDataStart = metadata.layerAndMaskInfoStart + layerAndMaskInfoLength + (header.largeFormat ? 8 : 4);

View File

@ -69,12 +69,10 @@ final public class PSDImageReaderSpi extends ImageReaderSpiBase {
switch (version) {
case PSD.VERSION_PSD:
case PSD.VERSION_PSB:
break;
default:
return false;
}
return true;
default:
// Fall through
}
}
return false;

View File

@ -0,0 +1,39 @@
package com.twelvemonkeys.imageio.plugins.psd;
import javax.imageio.ImageWriteParam;
import java.util.Locale;
/**
* PSDImageWriteParam
*/
public final class PSDImageWriteParam extends ImageWriteParam {
PSDImageWriteParam() {
this(Locale.getDefault());
}
PSDImageWriteParam(final Locale locale) {
super(locale);
compressionTypes = new String[] {
"None",
"PackBits",
// Two ZIP compression types are defined in spec, never seen in the wild...
// "ZIP",
// "ZIP+Predictor",
};
compressionType = compressionTypes[1];
canWriteCompressed = true;
}
static int getCompressionType(final ImageWriteParam param) {
if (param == null || param.getCompressionMode() != MODE_EXPLICIT || param.getCompressionType() == null || param.getCompressionType().equals("None")) {
return PSD.COMPRESSION_NONE;
}
else if (param.getCompressionType().equals("PackBits")) {
return PSD.COMPRESSION_RLE;
}
throw new IllegalArgumentException(String.format("Unsupported compression type: %s", param.getCompressionType()));
}
}

View File

@ -0,0 +1,369 @@
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.ImageWriterBase;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
import com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.RasterUtils;
import com.twelvemonkeys.io.enc.EncoderStream;
import com.twelvemonkeys.io.enc.PackBitsEncoder;
import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.*;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteOrder;
import java.util.Collections;
/**
* Minimal ImageWriter for Adobe Photoshop Document (PSD) format.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDImageWriter.java,v 1.0 Apr 29, 2008 4:45:52 PM haraldk Exp$
* @see <a href="http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/">Adobe Photoshop File Formats Specification<a>
* @see <a href="http://www.fileformat.info/format/psd/egff.htm">Adobe Photoshop File Format Summary<a>
*/
public final class PSDImageWriter extends ImageWriterBase {
PSDImageWriter(ImageWriterSpi provider) {
super(provider);
}
@Override
public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) {
// TODO: Implement
return null;
}
@Override
public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) {
// TODO: Implement
return null;
}
@Override
public ImageWriteParam getDefaultWriteParam() {
return new PSDImageWriteParam(getLocale());
}
@Override
public void write(IIOMetadata streamMetadata, IIOImage iioImage, ImageWriteParam param) throws IOException {
assertOutput();
imageOutput.setByteOrder(ByteOrder.BIG_ENDIAN);
RenderedImage image = iioImage.getRenderedImage();
SampleModel sampleModel = image.getSampleModel();
int colorComponents = image.getColorModel().getColorSpace().getNumComponents();
int channels = sampleModel.getNumBands();
int width = image.getWidth();
int height = image.getHeight();
int bits = getBitsPerSample(sampleModel);
int mode = getColorMode(image.getColorModel());
// TODO: Allow stream metadata or param to force PSD/PSB (version 1/2)?
boolean largeFormat = width > PSDHeader.PSD_MAX_SIZE || height > PSDHeader.PSD_MAX_SIZE;
new PSDHeader(channels, width, height, bits, mode, largeFormat).write(imageOutput);
writeColorModeData(image, mode);
writeImageResources(image, mode);
// Length of the layer and mask information section. (**PSB** length is 8 bytes.)
// TODO: Write an empty dummy layer here, if there's alpha? See below... Or see if Photoshop handles alpha if no layers at all...
if (largeFormat) {
imageOutput.writeLong(0);
}
else {
imageOutput.writeInt(0);
}
processImageStarted(0);
// Image Data Section (composite layer only).
// The last section of a Photoshop file contains the image pixel data.
// Image data is stored in planar order: first all the red data, then all the green data, etc.
// Each plane is stored in scan-line order, with no pad bytes,
final int compression = PSDImageWriteParam.getCompressionType(param);
imageOutput.writeShort(compression);
long byteCountPos = imageOutput.getStreamPosition();
// PSB (large format) byte counts are actually 32 bit offsets, not 16 bit as described in spec
int[] byteCounts = new int[compression == PSD.COMPRESSION_RLE ? height * channels : 0];
imageOutput.skipBytes(byteCounts.length * (largeFormat ? 4 : 2));
// TODO: Loop over tiles?
for (int channel = 0; channel < channels; channel++) {
// TODO: Alpha issues:
// 1. Alpha channel is written (but not read, because there are no layers, and alpha is considered present only if layer count is negative)
// - Can we write a small hidden layer, just to have -1 layers?
// 2. Alpha needs to be premultiplied against white background (to avoid inverse halo)
Raster tile = sampleModel.getTransferType() == DataBuffer.TYPE_INT && sampleModel instanceof SinglePixelPackedSampleModel
? RasterUtils.asByteRaster(image.getTile(0, 0))
: image.getTile(0, 0);
Raster channelRaster = tile.createChild(0, 0, width, height, 0, 0, new int[] {channel});
switch (bits) {
case 1:
// TODO: Figure out why we can't write multi-pixel packed 1 bit samples as bytes...
case 8:
write8BitChannel(channel, colorComponents, mode, compression, channelRaster, byteCounts);
break;
case 16:
write16BitChannel(channel, colorComponents, mode, compression, channelRaster, byteCounts);
break;
case 32:
write32BitChannel(channel, colorComponents, mode, compression, channelRaster, byteCounts);
break;
default:
throw new AssertionError(); // Should be guarded against already
}
processImageProgress(channel * 100f / channels);
}
updateByteCounts(byteCountPos, byteCounts, largeFormat);
processImageComplete();
}
private void updateByteCounts(long byteCountPos, int[] byteCounts, boolean largeFormat) throws IOException {
if (byteCounts.length == 0) {
return;
}
// Update byte counts for RLE
long pos = imageOutput.getStreamPosition();
imageOutput.seek(byteCountPos);
if (largeFormat) {
imageOutput.writeInts(byteCounts, 0, byteCounts.length);
}
else {
for (int byteCount : byteCounts) {
imageOutput.writeShort(byteCount);
}
}
imageOutput.seek(pos);
}
private void writeColorModeData(RenderedImage image, int mode) throws IOException {
if (mode == PSD.COLOR_MODE_INDEXED) {
IndexColorModel icm = (IndexColorModel) image.getColorModel();
// Indexed color images: length is 768; color data contains the color table for the image, in non-interleaved order.
imageOutput.writeInt(768);
byte[] colors = new byte[256];
icm.getReds(colors);
imageOutput.write(colors);
icm.getGreens(colors);
imageOutput.write(colors);
icm.getBlues(colors);
imageOutput.write(colors);
}
else {
imageOutput.writeInt(0);
}
}
private void writeImageResources(RenderedImage image, int mode) throws IOException {
// Length of image resource section. The length may be zero
imageOutput.writeInt(0);
long startImageResources = imageOutput.getStreamPosition();
// Write ICC color profile if not "native" sRGB or gray (or bitmap/indexed)
if (mode != PSD.COLOR_MODE_BITMAP && mode != PSD.COLOR_MODE_INDEXED) {
ColorSpace colorSpace = image.getColorModel().getColorSpace();
if (!colorSpace.isCS_sRGB() && colorSpace instanceof ICC_ColorSpace) {
ICC_Profile profile = ((ICC_ColorSpace) colorSpace).getProfile();
ICCProfile.writeData(imageOutput, profile);
}
}
// Write creator software (Exif)
Entry software = new TIFFEntry(TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, "TwelveMonkeys ImageIO PSD writer " + originatingProvider.getVersion());
PSDEXIF1Data.writeData(imageOutput, Collections.singleton(software));
long endImageResources = imageOutput.getStreamPosition();
// Update image resources length
imageOutput.seek(startImageResources - 4);
imageOutput.writeInt((int) (endImageResources - startImageResources));
imageOutput.seek(endImageResources);
}
private void write8BitChannel(int channel, int colorComponents, int colorMode, int compression, Raster raster, int[] byteCounts) throws IOException {
int width = raster.getWidth();
int height = raster.getHeight();
byte[] rowBytes = null;
for (int y = 0; y < height; y++) {
rowBytes = (byte[]) raster.getDataElements(0, y, width, 1, rowBytes);
// Photoshop likes to store CMYK values inverted (but not the alpha value)
if (colorMode == PSD.COLOR_MODE_CMYK && channel < colorComponents) {
for (int i = 0; i < rowBytes.length; i++) {
rowBytes[i] = (byte) (0xff - rowBytes[i] & 0xff);
}
}
if (compression == PSD.COMPRESSION_NONE) {
imageOutput.write(rowBytes);
}
else if (compression == PSD.COMPRESSION_RLE) {
long startPos = imageOutput.getStreamPosition();
// The RLE compressed data follows, with each scan line compressed separately
try (OutputStream stream = new EncoderStream(IIOUtil.createStreamAdapter(imageOutput), new PackBitsEncoder())) {
stream.write(rowBytes);
}
long endPos = imageOutput.getStreamPosition();
byteCounts[y + channel * height] = (int) (endPos - startPos);
}
else {
throw new IIOException("PSD with ZIP compression not supported");
}
}
}
private void write16BitChannel(int channel, int colorComponents, int colorMode, int compression, Raster raster, int[] byteCounts) throws IOException {
int width = raster.getWidth();
int height = raster.getHeight();
short[] row = null;
for (int y = 0; y < height; y++) {
row = (short[]) raster.getDataElements(0, y, width, 1, row);
// Photoshop likes to store CMYK values inverted (but not the alpha value)
if (colorMode == PSD.COLOR_MODE_CMYK && channel < colorComponents) {
for (int i = 0; i < row.length; i++) {
row[i] = (short) (0xffff - row[i] & 0xffff);
}
}
if (compression == PSD.COMPRESSION_NONE) {
imageOutput.writeShorts(row, 0, row.length);
}
else if (compression == PSD.COMPRESSION_RLE) {
long startPos = imageOutput.getStreamPosition();
// The RLE compressed data follows, with each scan line compressed separately
try (DataOutputStream stream = new DataOutputStream(new EncoderStream(IIOUtil.createStreamAdapter(imageOutput), new PackBitsEncoder()))) {
for (short sample : row) {
stream.writeShort(sample);
}
}
long endPos = imageOutput.getStreamPosition();
byteCounts[y + channel * height] = (int) (endPos - startPos);
}
else {
throw new IIOException("PSD with ZIP compression not supported");
}
}
}
private void write32BitChannel(int channel, int colorComponents, int colorMode, int compression, Raster raster, int[] byteCounts) throws IOException {
int width = raster.getWidth();
int height = raster.getHeight();
int[] row = null;
for (int y = 0; y < height; y++) {
row = (int[]) raster.getDataElements(0, y, width, 1, row);
// Photoshop likes to store CMYK values inverted (but not the alpha value)
if (colorMode == PSD.COLOR_MODE_CMYK && channel < colorComponents) {
for (int i = 0; i < row.length; i++) {
row[i] = 0xffffffff - row[i];
}
}
if (compression == PSD.COMPRESSION_NONE) {
imageOutput.writeInts(row, 0, row.length);
}
else if (compression == PSD.COMPRESSION_RLE) {
long startPos = imageOutput.getStreamPosition();
// The RLE compressed data follows, with each scan line compressed separately
try (DataOutputStream stream = new DataOutputStream(new EncoderStream(IIOUtil.createStreamAdapter(imageOutput), new PackBitsEncoder()))) {
for (int sample : row) {
stream.writeInt(sample);
}
}
long endPos = imageOutput.getStreamPosition();
byteCounts[y + channel * height] = (int) (endPos - startPos);
}
else {
throw new IIOException("PSD with ZIP compression not supported");
}
}
}
static int getColorMode(ColorModel colorModel) {
if (colorModel instanceof IndexColorModel) {
if (colorModel.getPixelSize() == 1) {
return PSD.COLOR_MODE_BITMAP;
}
else {
return PSD.COLOR_MODE_INDEXED;
}
}
int csType = colorModel.getColorSpace().getType();
switch (csType) {
case ColorSpace.TYPE_GRAY:
if (colorModel.getPixelSize() == 1) {
return PSD.COLOR_MODE_BITMAP;
}
else {
return PSD.COLOR_MODE_GRAYSCALE;
}
case ColorSpace.TYPE_RGB:
return PSD.COLOR_MODE_RGB;
case ColorSpace.TYPE_CMYK:
return PSD.COLOR_MODE_CMYK;
default:
throw new IllegalArgumentException("Unsupported color space type for PSD: " + csType);
}
}
static int getBitsPerSample(SampleModel sampleModel) {
int bits = sampleModel.getSampleSize(0);
for (int i = 1; i < sampleModel.getNumBands(); i++) {
if (bits != sampleModel.getSampleSize(i)) {
throw new IllegalArgumentException("All samples must be of equal size for PSD: " + bits);
}
}
switch (bits) {
case 1:
case 8:
case 16:
case 32:
return (short) bits;
default:
throw new IllegalArgumentException("Unsupported sample size for PSD (expected 1, 8, 16 or 32): " + bits);
}
}
public static void main(String[] args) throws IOException {
BufferedImage image = ImageIO.read(new File(args[0]));
ImageIO.write(image, "PSD", new File("test.psd"));
}
}

View File

@ -0,0 +1,47 @@
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.spi.ImageWriterSpiBase;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
import java.util.Locale;
import static com.twelvemonkeys.imageio.plugins.psd.PSDImageWriter.getBitsPerSample;
import static com.twelvemonkeys.imageio.plugins.psd.PSDImageWriter.getColorMode;
/**
* PSDImageWriterSpi
*/
public final class PSDImageWriterSpi extends ImageWriterSpiBase {
public PSDImageWriterSpi() {
super(new PSDProviderInfo());
}
@Override
public boolean canEncodeImage(ImageTypeSpecifier type) {
// PSD supports:
// - 1, 8, 16 or 32 bit/sample
// - Number of samples <= 56
// - RGB, CMYK, Gray, Indexed color
try {
getBitsPerSample(type.getSampleModel());
getColorMode(type.getColorModel());
}
catch (IllegalArgumentException ignore) {
// We can't write this type
return false;
}
return type.getNumBands() <= 56; // Can't be negative
}
@Override
public ImageWriter createWriterInstance(Object extension) {
return new PSDImageWriter(this);
}
public String getDescription(final Locale pLocale) {
return "Adobe Photoshop Document (PSD) image writer";
}
}

View File

@ -43,8 +43,8 @@ final class PSDProviderInfo extends ReaderWriterProviderInfo {
protected PSDProviderInfo() {
super(
PSDProviderInfo.class,
new String[] {"psd", "PSD"},
new String[] {"psd"},
new String[] {"psd", "PSD", "psb", "PSB"},
new String[] {"psd", "psb"},
new String[] {
"image/vnd.adobe.photoshop", // Official, IANA registered
"application/vnd.adobe.photoshop", // Used in XMP
@ -54,8 +54,8 @@ final class PSDProviderInfo extends ReaderWriterProviderInfo {
},
"com.twelvemonkeys.imageio.plugins.psd.PSDImageReader",
new String[] {"com.twelvemonkeys.imageio.plugins.psd.PSDImageReaderSpi"},
null,
null, // new String[] {"com.twelvemonkeys.imageio.plugins.psd.PSDImageWriterSpi"},
"com.twelvemonkeys.imageio.plugins.psd.PSDImageWriter",
new String[] {"com.twelvemonkeys.imageio.plugins.psd.PSDImageWriterSpi"},
false, null, null, null, null,
true, PSDMetadata.NATIVE_METADATA_FORMAT_NAME, PSDMetadata.NATIVE_METADATA_FORMAT_CLASS_NAME, null, null
);

View File

@ -0,0 +1 @@
com.twelvemonkeys.imageio.plugins.psd.PSDImageWriterSpi

View File

@ -0,0 +1,37 @@
package com.twelvemonkeys.imageio.plugins.psd;
import com.twelvemonkeys.imageio.util.ImageWriterAbstractTest;
import javax.imageio.spi.ImageWriterSpi;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.util.Arrays;
import java.util.List;
/**
* PSDImageWriterTest.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: PSDImageWriterTest.java,v 1.0 05/05/2021 haraldk Exp$
*/
public class PSDImageWriterTest extends ImageWriterAbstractTest<PSDImageWriter> {
@Override
protected ImageWriterSpi createProvider() {
return new PSDImageWriterSpi();
}
@Override
protected List<? extends RenderedImage> getTestData() {
return Arrays.asList(
new BufferedImage(300, 200, BufferedImage.TYPE_INT_RGB),
new BufferedImage(301, 199, BufferedImage.TYPE_INT_ARGB),
new BufferedImage(299, 201, BufferedImage.TYPE_3BYTE_BGR),
new BufferedImage(160, 90, BufferedImage.TYPE_4BYTE_ABGR),
new BufferedImage(90, 160, BufferedImage.TYPE_BYTE_GRAY),
new BufferedImage(30, 20, BufferedImage.TYPE_USHORT_GRAY),
new BufferedImage(30, 20, BufferedImage.TYPE_BYTE_BINARY),
new BufferedImage(30, 20, BufferedImage.TYPE_BYTE_INDEXED)
);
}
}

View File

@ -86,13 +86,13 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest<TIFFImageWriter
protected List<? extends RenderedImage> getTestData() {
return Arrays.asList(
new BufferedImage(300, 200, BufferedImage.TYPE_INT_RGB),
new BufferedImage(300, 200, BufferedImage.TYPE_INT_ARGB),
new BufferedImage(300, 200, BufferedImage.TYPE_3BYTE_BGR),
new BufferedImage(300, 200, BufferedImage.TYPE_4BYTE_ABGR),
new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_GRAY),
new BufferedImage(300, 200, BufferedImage.TYPE_USHORT_GRAY),
new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_BINARY),
new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_INDEXED)
new BufferedImage(301, 199, BufferedImage.TYPE_INT_ARGB),
new BufferedImage(299, 201, BufferedImage.TYPE_3BYTE_BGR),
new BufferedImage(160, 90, BufferedImage.TYPE_4BYTE_ABGR),
new BufferedImage(90, 160, BufferedImage.TYPE_BYTE_GRAY),
new BufferedImage(30, 20, BufferedImage.TYPE_USHORT_GRAY),
new BufferedImage(30, 20, BufferedImage.TYPE_BYTE_BINARY),
new BufferedImage(30, 20, BufferedImage.TYPE_BYTE_INDEXED)
);
}

View File

@ -1,90 +0,0 @@
package com.twelvemonkeys.imageio.plugins.webp;
import java.awt.*;
import java.awt.image.*;
import static com.twelvemonkeys.lang.Validate.notNull;
/**
* RasterUtils
*/
public final class RasterUtils {
// TODO: Generalize and move to common util package
private RasterUtils() {}
public static WritableRaster asByteRaster(final WritableRaster raster, final ColorModel colorModel) {
switch (raster.getTransferType()) {
case DataBuffer.TYPE_BYTE:
return raster;
case DataBuffer.TYPE_INT:
final int bands = colorModel.getNumComponents();
final DataBufferInt buffer = (DataBufferInt) raster.getDataBuffer();
int w = raster.getWidth();
int h = raster.getHeight();
int size = buffer.getSize();
return new WritableRaster(
new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, w, h, bands, w * bands, createBandOffsets(colorModel)),
new DataBuffer(DataBuffer.TYPE_BYTE, size * bands) {
// TODO: These masks should probably not be hardcoded
final int[] MASKS = {
0xffffff00,
0xffff00ff,
0xff00ffff,
0x00ffffff,
};
@Override
public int getElem(int bank, int i) {
int index = i / bands;
int shift = (i % bands) * 8;
return (buffer.getElem(index) >>> shift) & 0xff;
}
@Override
public void setElem(int bank, int i, int val) {
int index = i / bands;
int element = i % bands;
int shift = element * 8;
int value = (buffer.getElem(index) & MASKS[element]) | ((val & 0xff) << shift);
buffer.setElem(index, value);
}
}, new Point()) {};
default:
throw new IllegalArgumentException(String.format("Raster type %d not supported", raster.getTransferType()));
}
}
private static int[] createBandOffsets(final ColorModel colorModel) {
notNull(colorModel, "colorModel");
if (colorModel instanceof DirectColorModel) {
DirectColorModel dcm = (DirectColorModel) colorModel;
int[] masks = dcm.getMasks();
int[] offs = new int[masks.length];
for (int i = 0; i < masks.length; i++) {
int mask = masks[i];
int off = 0;
// TODO: FixMe! This only works for standard 8 bit masks (0xFF)
if (mask != 0) {
while ((mask & 0xFF) == 0) {
mask >>>= 8;
off++;
}
}
offs[i] = off;
}
return offs;
}
throw new IllegalArgumentException(String.format("%s not supported", colorModel.getClass().getSimpleName()));
}
}

View File

@ -42,6 +42,7 @@ import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.imageio.util.RasterUtils;
import javax.imageio.IIOException;
import javax.imageio.ImageReadParam;
@ -299,6 +300,7 @@ final class WebPImageReader extends ImageReaderBase {
List<ImageTypeSpecifier> types = new ArrayList<>();
types.add(rawImageType);
types.add(ImageTypeSpecifiers.createFromBufferedImageType(header.containsALPH ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB));
types.add(ImageTypeSpecifiers.createFromBufferedImageType(header.containsALPH ? BufferedImage.TYPE_INT_ARGB_PRE : BufferedImage.TYPE_INT_BGR));
return types.iterator();
}
@ -314,13 +316,13 @@ final class WebPImageReader extends ImageReaderBase {
switch (header.fourCC) {
case WebP.CHUNK_VP8_:
imageInput.seek(header.offset);
readVP8(RasterUtils.asByteRaster(destination.getRaster(), destination.getColorModel()), param);
readVP8(RasterUtils.asByteRaster(destination.getRaster()), param);
break;
case WebP.CHUNK_VP8L:
imageInput.seek(header.offset);
readVP8Lossless(RasterUtils.asByteRaster(destination.getRaster(), destination.getColorModel()), param);
readVP8Lossless(RasterUtils.asByteRaster(destination.getRaster()), param);
break;
@ -373,13 +375,13 @@ final class WebPImageReader extends ImageReaderBase {
break;
case WebP.CHUNK_VP8_:
readVP8(RasterUtils.asByteRaster(destination.getRaster(), destination.getColorModel())
readVP8(RasterUtils.asByteRaster(destination.getRaster())
.createWritableChild(0, 0, width, height, 0, 0, new int[]{0, 1, 2}), param);
break;
case WebP.CHUNK_VP8L:
readVP8Lossless(RasterUtils.asByteRaster(destination.getRaster(), destination.getColorModel()), param);
readVP8Lossless(RasterUtils.asByteRaster(destination.getRaster()), param);
break;

View File

@ -40,7 +40,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static com.twelvemonkeys.imageio.plugins.webp.RasterUtils.asByteRaster;
import static com.twelvemonkeys.imageio.util.RasterUtils.asByteRaster;
import static java.lang.Math.*;
/**
@ -180,8 +180,7 @@ public final class VP8LDecoder {
new DataBufferInt(colorTable, colorTableSize),
colorTableSize, 1, colorTableSize,
new int[] {0}, null
),
ColorModel.getRGBdefault()
)
), false);
// TODO: We may not really need this value...