IFF: Read support for Impulse (Imagine, Turbo Silver) RGB8 format.

This commit is contained in:
Harald Kuhr 2022-01-28 16:36:34 +01:00
parent 1271a3d55e
commit e17faad6fb
8 changed files with 297 additions and 30 deletions

View File

@ -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);

View File

@ -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;
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
if (colorMap != null) {
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));
}

View File

@ -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;
}

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: RGB8Stream.java,v 1.0 28/01/2022 haraldk Exp$
*
* @see <a href="https://wiki.amigaos.net/wiki/RGBN_and_RGB8_IFF_Image_Data">RGBN and RGB8 IFF Image Data</a>
*/
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();
}
}

View File

@ -88,7 +88,11 @@ public class IFFImageReaderTest extends ImageReaderAbstractTest<IFFImageReader>
// 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))
);
}

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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));
}
}
}