mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-04 12:05:29 -04:00
IFF: Read support for Impulse (Imagine, Turbo Silver) RGB8 format.
This commit is contained in:
parent
1271a3d55e
commit
e17faad6fb
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
BIN
imageio/imageio-iff/src/test/resources/iff/glowsphere2.rgb8
Normal file
BIN
imageio/imageio-iff/src/test/resources/iff/glowsphere2.rgb8
Normal file
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user