diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageMetadata.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageMetadata.java index 7ded0a4c..9634084a 100644 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageMetadata.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageMetadata.java @@ -62,6 +62,7 @@ final class IFFImageMetadata extends AbstractMetadata { case 6: case 7: case 24: + case 25: case 32: csType.setAttribute("name", "RGB"); break; @@ -145,6 +146,7 @@ final class IFFImageMetadata extends AbstractMetadata { // PlanarConfiguration IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration"); switch (formType) { + case TYPE_RGB8: case TYPE_PBM: planarConfiguration.setAttribute("value", "PixelInterleaved"); break; @@ -187,10 +189,16 @@ final class IFFImageMetadata extends AbstractMetadata { return Integer.toString(bitplanes); case 24: return "8 8 8"; + case 25: + if (formType != TYPE_RGB8) { + throw new IllegalArgumentException(String.format("25 bit depth only supported for FORM type RGB8: %s", IFFUtil.toChunkStr(formType))); + } + + return "8 8 8 1"; case 32: return "8 8 8 8"; default: - throw new IllegalArgumentException("Ubknown bit count: " + bitplanes); + throw new IllegalArgumentException("Unknown bit count: " + bitplanes); } } @@ -246,13 +254,14 @@ final class IFFImageMetadata extends AbstractMetadata { @Override protected IIOMetadataNode getStandardTransparencyNode() { - if ((colorMap == null || !colorMap.hasAlpha()) && header.bitplanes != 32) { + // TODO: Make sure 25 bit is only RGB8... + if ((colorMap == null || !colorMap.hasAlpha()) && header.bitplanes != 32 && header.bitplanes != 25) { return null; } IIOMetadataNode transparency = new IIOMetadataNode("Transparency"); - if (header.bitplanes == 32) { + if (header.bitplanes == 25 || header.bitplanes == 32) { IIOMetadataNode alpha = new IIOMetadataNode("Alpha"); alpha.setAttribute("value", "nonpremultiplied"); transparency.appendChild(alpha); diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java index d3c8f6ef..6c752711 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReader.java @@ -32,6 +32,7 @@ package com.twelvemonkeys.imageio.plugins.iff; import com.twelvemonkeys.image.ResampleOp; import com.twelvemonkeys.imageio.ImageReaderBase; +import com.twelvemonkeys.imageio.color.ColorSpaces; import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; import com.twelvemonkeys.io.enc.DecoderStream; @@ -154,7 +155,7 @@ public final class IFFImageReader extends ImageReaderBase { int remaining = imageInput.readInt() - 4; // We'll read 4 more in a sec formType = imageInput.readInt(); - if (formType != IFF.TYPE_ILBM && formType != IFF.TYPE_PBM/* && formType != IFF.TYPE_DEEP*/) { + if (formType != IFF.TYPE_ILBM && formType != IFF.TYPE_PBM && formType != IFF.TYPE_RGB8 && formType != IFF.TYPE_DEEP) { throw new IIOException(String.format("Only IFF FORM types 'ILBM' and 'PBM ' supported: %s", IFFUtil.toChunkStr(formType))); } @@ -381,26 +382,32 @@ public final class IFFImageReader extends ImageReaderBase { if (!isConvertToRGB()) { if (colorMap != null) { IndexColorModel cm = colorMap.getIndexColorModel(header, isEHB()); - specifier = ImageTypeSpecifiers.createFromIndexColorModel(cm); + return ImageTypeSpecifiers.createFromIndexColorModel(cm); } else { - specifier = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY); + return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY); } - break; } // NOTE: HAM modes falls through, as they are converted to RGB case 24: // 24 bit RGB - specifier = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR); - break; + return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR); + + case 25: // TYPE_RGB8: 24 bit + 1 bit mask (we'll convert to full alpha during decoding) + if (formType != IFF.TYPE_RGB8) { + throw new IIOException(String.format("25 bit depth only supported for FORM type RGB8: %s", IFFUtil.toChunkStr(formType))); + } + + return ImageTypeSpecifiers.createInterleaved(ColorSpaces.getColorSpace(ColorSpace.CS_sRGB), + new int[] {0, 1, 2, 3}, DataBuffer.TYPE_BYTE, true, false); + case 32: // 32 bit ARGB - specifier = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR); - break; + return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR); + default: throw new IIOException(String.format("Bit depth not implemented: %d", header.bitplanes)); } - return specifier; } private boolean isConvertToRGB() { @@ -411,15 +418,17 @@ public final class IFFImageReader extends ImageReaderBase { imageInput.seek(bodyStart); byteRunStream = null; - // NOTE: colorMap may be null for 8 bit (gray), 24 bit or 32 bit only - if (colorMap != null) { + if (formType == IFF.TYPE_RGB8) { + readRGB8(pParam, imageInput); + } + else if (colorMap != null) { + // NOTE: colorMap may be null for 8 bit (gray), 24 bit or 32 bit only IndexColorModel cm = colorMap.getIndexColorModel(header, isEHB()); readIndexed(pParam, imageInput, cm); } else { readTrueColor(pParam, imageInput); } - } private void readIndexed(final ImageReadParam pParam, final ImageInputStream pInput, final IndexColorModel pModel) throws IOException { @@ -481,12 +490,7 @@ public final class IFFImageReader extends ImageReaderBase { Raster sourceRow = raster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, sourceBands); final byte[] row = new byte[width * 8]; - -// System.out.println("PlaneData length: " + planeData.length); -// System.out.println("Row length: " + row.length); - final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); - final int planes = header.bitplanes; Object dataElements = null; @@ -536,8 +540,6 @@ public final class IFFImageReader extends ImageReaderBase { // Rasters are compatible, just write to destination if (sourceXSubsampling == 1) { destination.setRect(offset.x, dstY, sourceRow); -// dataElements = raster.getDataElements(aoi.x, 0, aoi.width, 1, dataElements); -// destination.setDataElements(offset.x, offset.y + (srcY - aoi.y) / sourceYSubsampling, aoi.width, 1, dataElements); } else { for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) { @@ -581,6 +583,71 @@ public final class IFFImageReader extends ImageReaderBase { } } + private void readRGB8(ImageReadParam pParam, ImageInputStream pInput) throws IOException { + final int width = header.width; + final int height = header.height; + + final Rectangle aoi = getSourceRegion(pParam, width, height); + final Point offset = pParam == null ? new Point(0, 0) : pParam.getDestinationOffset(); + + // Set everything to default values + int sourceXSubsampling = 1; + int sourceYSubsampling = 1; + int[] sourceBands = null; + int[] destinationBands = null; + + // Get values from the ImageReadParam, if any + if (pParam != null) { + sourceXSubsampling = pParam.getSourceXSubsampling(); + sourceYSubsampling = pParam.getSourceYSubsampling(); + + sourceBands = pParam.getSourceBands(); + destinationBands = pParam.getDestinationBands(); + } + + // Ensure band settings from param are compatible with images + checkReadParamBandSettings(pParam, 4, image.getSampleModel().getNumBands()); + + WritableRaster destination = image.getRaster(); + if (destinationBands != null || offset.x != 0 || offset.y != 0) { + destination = destination.createWritableChild(0, 0, destination.getWidth(), destination.getHeight(), offset.x, offset.y, destinationBands); + } + + WritableRaster raster = image.getRaster().createCompatibleWritableRaster(width, 1); + Raster sourceRow = raster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, sourceBands); + + int planeWidth = width * 4; + + final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData(); + final int channels = (header.bitplanes + 7) / 8; + + Object dataElements = null; + + for (int srcY = 0; srcY < height; srcY++) { + readPlaneData(pInput, data, 0, planeWidth); + + if (srcY >= aoi.y && (srcY - aoi.y) % sourceYSubsampling == 0) { + int dstY = (srcY - aoi.y) / sourceYSubsampling; + if (sourceXSubsampling == 1) { + destination.setRect(0, dstY, sourceRow); + } + else { + for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) { + dataElements = sourceRow.getDataElements(srcX, 0, dataElements); + int dstX = srcX / sourceXSubsampling; + destination.setDataElements(dstX, dstY, dataElements); + } + } + } + + processImageProgress(srcY * 100f / header.width); + if (abortRequested()) { + processReadAborted(); + break; + } + } + } + // One row from each of the 24 bitplanes is written before moving to the // next scanline. For each scanline, the red bitplane rows are stored first, // followed by green and blue. The first plane holds the least significant @@ -721,6 +788,24 @@ public final class IFFImageReader extends ImageReaderBase { byteRunStream.readFully(pData, pOffset, pPlaneWidth); break; + case 4: // Compression type 4 means different things for different FORM types... :-P + if (formType == IFF.TYPE_RGB8) { + // Impulse RGB8 RLE compression: 24 bit RGB + 1 bit mask + 7 bit run count + if (byteRunStream == null) { + byteRunStream = new DataInputStream( + new DecoderStream( + IIOUtil.createStreamAdapter(pInput, body.chunkLength), + new RGB8RLEDecoder(), + pPlaneWidth * 4 + ) + ); + } + + byteRunStream.readFully(pData, pOffset, pPlaneWidth); + + break; + } + default: throw new IIOException(String.format("Unknown compression type: %d", header.compressionType)); } diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderSpi.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderSpi.java index 92b2ec67..36e196a5 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderSpi.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderSpi.java @@ -30,13 +30,12 @@ package com.twelvemonkeys.imageio.plugins.iff; -import java.io.IOException; -import java.util.Locale; +import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; - -import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; +import java.io.IOException; +import java.util.Locale; /** * IFFImageReaderSpi @@ -67,9 +66,8 @@ public final class IFFImageReaderSpi extends ImageReaderSpiBase { pInput.readInt();// Skip length field int type = pInput.readInt(); - - // Is it ILBM or PBM - if (type == IFF.TYPE_ILBM || type == IFF.TYPE_PBM) { + if (type == IFF.TYPE_ILBM || type == IFF.TYPE_PBM + || type == IFF.TYPE_RGB8) { // Impulse RGB8 return true; } diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/RGB8RLEDecoder.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/RGB8RLEDecoder.java new file mode 100644 index 00000000..461022b9 --- /dev/null +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/RGB8RLEDecoder.java @@ -0,0 +1,57 @@ +package com.twelvemonkeys.imageio.plugins.iff; + +import com.twelvemonkeys.io.enc.DecodeException; +import com.twelvemonkeys.io.enc.Decoder; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * Decoder implementation for Impulse FORM RGB8 RLE compression (type 4). + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: RGB8Stream.java,v 1.0 28/01/2022 haraldk Exp$ + * + * @see RGBN and RGB8 IFF Image Data + */ +final class RGB8RLEDecoder implements Decoder { + public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException { + while (buffer.remaining() >= 127 * 4) { + int r = stream.read(); + int g = stream.read(); + int b = stream.read(); + int a = stream.read(); + + if (a < 0) { + // Normal EOF + if (r == -1) { + break; + } + + // Partial pixel read... + throw new EOFException(); + } + + // Get "genlock" (transparency) bit + count + boolean alpha = (a & 0x80) != 0; + int count = a & 0x7f; + a = alpha ? 0 : (byte) 0xff; // convert to full transparent/opaque; + + if (count == 0) { + throw new DecodeException("Multi-byte counts not supported"); + } + + for (int i = 0; i < count; i++) { + buffer.put((byte) r); + buffer.put((byte) g); + buffer.put((byte) b); + buffer.put((byte) a); + } + } + + return buffer.position(); + } +} diff --git a/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderTest.java b/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderTest.java index db1c1993..1ca3102f 100755 --- a/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderTest.java +++ b/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderTest.java @@ -88,7 +88,11 @@ public class IFFImageReaderTest extends ImageReaderAbstractTest // 16 color indexed, multi palette (PCHG) - Ok new TestData(getClassLoaderResource("/iff/Manhattan.PCHG"), new Dimension(704, 440)), // 16 color indexed, multi palette (PCHG + SHAM) - Ok - new TestData(getClassLoaderResource("/iff/Somnambulist-2.SHAM"), new Dimension(704, 440)) + new TestData(getClassLoaderResource("/iff/Somnambulist-2.SHAM"), new Dimension(704, 440)), + // Impulse RGB8 format straight from Imagine 2.0 + new TestData(getClassLoaderResource("/iff/glowsphere2.rgb8"), new Dimension(640, 480)), + // Impulse RGB8 format written by ASDG ADPro, with cross boundary runs, which is probably not as per spec... + new TestData(getClassLoaderResource("/iff/tunnel04-adpro-cross-boundary-runs.rgb8"), new Dimension(640, 480)) ); } diff --git a/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/RGB8RLEDecoderTest.java b/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/RGB8RLEDecoderTest.java new file mode 100644 index 00000000..c45d357f --- /dev/null +++ b/imageio/imageio-iff/src/test/java/com/twelvemonkeys/imageio/plugins/iff/RGB8RLEDecoderTest.java @@ -0,0 +1,114 @@ +package com.twelvemonkeys.imageio.plugins.iff; + +import com.twelvemonkeys.io.enc.DecodeException; +import com.twelvemonkeys.io.enc.Decoder; +import com.twelvemonkeys.io.enc.DecoderAbstractTest; +import com.twelvemonkeys.io.enc.Encoder; + +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * RGB8RLEDecoderTest. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: RGB8RLEDecoderTest.java,v 1.0 28/01/2022 haraldk Exp$ + */ +public class RGB8RLEDecoderTest extends DecoderAbstractTest { + + public static final int BUFFER_SIZE = 1024; + + @Override + public Decoder createDecoder() { + return new RGB8RLEDecoder(); + } + + @Override + public Encoder createCompatibleEncoder() { + return null; + } + + @Test + public final void testDecodeEmpty() throws IOException { + Decoder decoder = createDecoder(); + ByteArrayInputStream bytes = new ByteArrayInputStream(new byte[0]); + + int count = decoder.decode(bytes, ByteBuffer.allocate(BUFFER_SIZE)); + assertEquals("Should not be able to read any bytes", 0, count); + } + + @Test(expected = EOFException.class) + public final void testDecodePartial() throws IOException { + Decoder decoder = createDecoder(); + ByteArrayInputStream bytes = new ByteArrayInputStream(new byte[] {0}); + + decoder.decode(bytes, ByteBuffer.allocate(BUFFER_SIZE)); + fail("Should not be able to read any bytes"); + } + + @Test(expected = EOFException.class) + public final void testDecodePartialToo() throws IOException { + Decoder decoder = createDecoder(); + ByteArrayInputStream bytes = new ByteArrayInputStream(new byte[] {0, 0, 0, 1, 0, 0}); + + decoder.decode(bytes, ByteBuffer.allocate(BUFFER_SIZE)); + fail("Should not be able to read any bytes"); + } + + @Test(expected = DecodeException.class) + public final void testDecodeZeroRun() throws IOException { + // The spec says that 0-runs should be used to signal that the run is > 127, + // and contained in the next byte, however, this is not used in practise and not supported. + Decoder decoder = createDecoder(); + ByteArrayInputStream bytes = new ByteArrayInputStream(new byte[] {0, 0, 0, 0}); + + decoder.decode(bytes, ByteBuffer.allocate(BUFFER_SIZE)); + fail("Should not be able to read any bytes"); + } + + @Test + public final void testDecodeSingleOpaque() throws IOException { + Decoder decoder = createDecoder(); + ByteArrayInputStream bytes = new ByteArrayInputStream(new byte[] {0, 0, 0, 1}); + ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); + + int count = decoder.decode(bytes, buffer); + + assertEquals(4, count); + assertEquals(0x000000FF, buffer.getInt(0)); + } + + @Test + public final void testDecodeSingleTransparent() throws IOException { + Decoder decoder = createDecoder(); + ByteArrayInputStream bytes = new ByteArrayInputStream(new byte[] {0, 0, 0, (byte) 0x81}); + ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); + + int count = decoder.decode(bytes, buffer); + + assertEquals(4, count); + assertEquals(0x00000000, buffer.getInt(0)); + } + + @Test + public final void testDecodeMaxRun() throws IOException { + Decoder decoder = createDecoder(); + ByteArrayInputStream bytes = new ByteArrayInputStream(new byte[] {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x7F}); + ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); + + int count = decoder.decode(bytes, buffer); + + assertEquals(127 * 4, count); + for (int i = 0; i < 127; i++) { + assertEquals(0xFFFFFFFF, buffer.getInt(i)); + } + } +} \ No newline at end of file diff --git a/imageio/imageio-iff/src/test/resources/iff/glowsphere2.rgb8 b/imageio/imageio-iff/src/test/resources/iff/glowsphere2.rgb8 new file mode 100644 index 00000000..c6a08e50 Binary files /dev/null and b/imageio/imageio-iff/src/test/resources/iff/glowsphere2.rgb8 differ diff --git a/imageio/imageio-iff/src/test/resources/iff/tunnel04-adpro-cross-boundary-runs.rgb8 b/imageio/imageio-iff/src/test/resources/iff/tunnel04-adpro-cross-boundary-runs.rgb8 new file mode 100644 index 00000000..22bdefea Binary files /dev/null and b/imageio/imageio-iff/src/test/resources/iff/tunnel04-adpro-cross-boundary-runs.rgb8 differ