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 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 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 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...