diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/RasterUtils.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/RasterUtils.java
new file mode 100644
index 00000000..493549ea
--- /dev/null
+++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/RasterUtils.java
@@ -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;
+ }
+}
diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/RasterUtilsTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/RasterUtilsTest.java
new file mode 100644
index 00000000..13ec1faa
--- /dev/null
+++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/RasterUtilsTest.java
@@ -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 Harald Kuhr
+ * @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));
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFWriter.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFWriter.java
index 2755b35e..f8ed4104 100644
--- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFWriter.java
+++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFWriter.java
@@ -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 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);
}
diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/ICCProfile.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/ICCProfile.java
index 2c68b46b..dd8f8e25 100755
--- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/ICCProfile.java
+++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/ICCProfile.java
@@ -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
}
}
diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java
index d0d31eca..ed23ca75 100644
--- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java
+++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDEXIF1Data.java
@@ -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();
diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDHeader.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDHeader.java
index ac931264..1bad65a3 100755
--- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDHeader.java
+++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDHeader.java
@@ -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";
}
}
+
}
diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java
index 2c8df8fb..5bf608e0 100644
--- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java
+++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReader.java
@@ -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);
diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderSpi.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderSpi.java
index d6924419..b41fed85 100755
--- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderSpi.java
+++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageReaderSpi.java
@@ -69,12 +69,10 @@ final public class PSDImageReaderSpi extends ImageReaderSpiBase {
switch (version) {
case PSD.VERSION_PSD:
case PSD.VERSION_PSB:
- break;
+ return true;
default:
- return false;
+ // Fall through
}
-
- return true;
}
return false;
diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageWriteParam.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageWriteParam.java
new file mode 100644
index 00000000..500310af
--- /dev/null
+++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageWriteParam.java
@@ -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()));
+ }
+}
diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageWriter.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageWriter.java
new file mode 100644
index 00000000..44a8b1bb
--- /dev/null
+++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageWriter.java
@@ -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 Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: PSDImageWriter.java,v 1.0 Apr 29, 2008 4:45:52 PM haraldk Exp$
+ * @see Adobe Photoshop File Formats Specification
+ * @see Adobe Photoshop File Format Summary
+ */
+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"));
+ }
+}
diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageWriterSpi.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageWriterSpi.java
new file mode 100644
index 00000000..b6d96e96
--- /dev/null
+++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageWriterSpi.java
@@ -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";
+ }
+}
diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDProviderInfo.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDProviderInfo.java
index cdf26a9b..fc9f5ed3 100644
--- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDProviderInfo.java
+++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDProviderInfo.java
@@ -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
);
diff --git a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDVersionInfo.java b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDVersionInfo.java
index 1655c381..441c54d8 100644
--- a/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDVersionInfo.java
+++ b/imageio/imageio-psd/src/main/java/com/twelvemonkeys/imageio/plugins/psd/PSDVersionInfo.java
@@ -67,7 +67,7 @@ final class PSDVersionInfo extends PSDImageResource {
writer = PSDUtil.readUnicodeString(pInput);
reader = PSDUtil.readUnicodeString(pInput);
-
+
fileVersion = pInput.readInt();
}
diff --git a/imageio/imageio-psd/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi b/imageio/imageio-psd/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi
new file mode 100644
index 00000000..63c630ff
--- /dev/null
+++ b/imageio/imageio-psd/src/main/resources/META-INF/services/javax.imageio.spi.ImageWriterSpi
@@ -0,0 +1 @@
+com.twelvemonkeys.imageio.plugins.psd.PSDImageWriterSpi
diff --git a/imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageWriterTest.java b/imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageWriterTest.java
new file mode 100644
index 00000000..ef84f9c3
--- /dev/null
+++ b/imageio/imageio-psd/src/test/java/com/twelvemonkeys/imageio/plugins/psd/PSDImageWriterTest.java
@@ -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 Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: PSDImageWriterTest.java,v 1.0 05/05/2021 haraldk Exp$
+ */
+public class PSDImageWriterTest extends ImageWriterAbstractTest {
+ @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)
+ );
+ }
+}
diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java
index 7993c7be..bce5ec02 100644
--- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java
+++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java
@@ -86,13 +86,13 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest 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)
);
}
diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/RasterUtils.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/RasterUtils.java
deleted file mode 100644
index 2f73dd39..00000000
--- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/RasterUtils.java
+++ /dev/null
@@ -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()));
- }
-}
diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java
index d5e4b07e..d257c869 100644
--- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java
+++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java
@@ -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 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;
diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/VP8LDecoder.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/VP8LDecoder.java
index bbb40eac..d25c3ee6 100644
--- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/VP8LDecoder.java
+++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/VP8LDecoder.java
@@ -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...