/*
* IFFCodec
*
* Copyright (c) 2000, 2001, 2002, 2003, 2004, 2005, 2006 Marco Schmidt.
* All rights reserved.
*/
package net.sourceforge.jiu.codecs;
import java.io.DataInput;
import java.io.IOException;
import net.sourceforge.jiu.codecs.ImageCodec;
import net.sourceforge.jiu.codecs.InvalidFileStructureException;
import net.sourceforge.jiu.codecs.UnsupportedTypeException;
import net.sourceforge.jiu.codecs.WrongFileFormatException;
import net.sourceforge.jiu.data.MemoryPaletted8Image;
import net.sourceforge.jiu.data.MemoryRGB24Image;
import net.sourceforge.jiu.data.PixelImage;
import net.sourceforge.jiu.data.Palette;
import net.sourceforge.jiu.data.Paletted8Image;
import net.sourceforge.jiu.data.RGB24Image;
import net.sourceforge.jiu.ops.MissingParameterException;
import net.sourceforge.jiu.ops.OperationFailedException;
import net.sourceforge.jiu.ops.WrongParameterException;
/**
* A codec to read Amiga IFF image files.
* IFF (Interchange File Format) is an Amiga wrapper file format for texts, images, animations, sound and other kinds of data.
* This codec only deals with image IFF files.
* Typical file extensions for IFF image files are <code>.lbm</code> and <code>.iff</code>.
* <h3>Loading / saving</h3>
* Only loading is supported by this codec.
* <h3>Supported file types</h3>
* Both uncompressed and run-length encoded files are read.
* <ul>
* <li>1 to 8 bit indexed (paletted) color</li>
* <li>24 bit RGB truecolor</li>
* <li>HAM6 and HAM8 images (which are a mixture of paletted and truecolor)</li>
* </ul>
* <h3>Usage example</h3>
* <pre>
* IFFCodec codec = new IFFCodec();
* codec.setFile("image.iff", CodecMode.LOAD);
* codec.process();
* PixelImage image = codec.getImage();
* </pre>
* @author Marco Schmidt
* @since 0.3.0
*/
public class IFFCodec extends ImageCodec
{
private final static int MAGIC_BMHD = 0x424d4844;
private final static int MAGIC_BODY = 0x424f4459;
private final static int MAGIC_CMAP = 0x434d4150;
private final static int MAGIC_CAMG = 0x43414d47;
private final static int MAGIC_FORM = 0x464f524d;
private final static int MAGIC_ILBM = 0x494c424d;
private final static int MAGIC_PBM = 0x50424d20;
private final static int SIZE_BMHD = 0x00000014;
private final static byte COMPRESSION_NONE = 0x00;
private final static byte COMPRESSION_RLE = 0x01;
private int camg;
private byte compression;
private boolean ehb;
private boolean ham;
private boolean ham6;
private boolean ham8;
private int height;
private int numPlanes;
private Palette palette;
private boolean rgb24;
private int type;
private int width;
private void checkAndLoad() throws
InvalidFileStructureException,
IOException,
MissingParameterException,
UnsupportedTypeException,
WrongFileFormatException,
WrongParameterException
{
DataInput in = getInputAsDataInput();
if (in == null)
{
throw new MissingParameterException("InputStream / DataInput object is missing.");
}
int formMagic = in.readInt();
if (formMagic != MAGIC_FORM)
{
throw new WrongFileFormatException("Cannot load image. The " +
"input stream is not a valid IFF file (wrong magic byte " +
"sequence).");
}
in.readInt(); // read and discard "file size" field
type = in.readInt();
if (type != MAGIC_ILBM && type != MAGIC_PBM)
{
throw new UnsupportedTypeException("Cannot load image. The " +
"input stream is an IFF file, but not of type ILBM or PBM" +
" (" + getChunkName(type) + ")");
}
PixelImage result = null;
boolean hasBMHD = false;
boolean hasCAMG = false;
do
{
int magic = in.readInt();
//System.out.println(chunkNameToString(magic));
int size = in.readInt();
// chunks must always have an even number of bytes
if ((size & 1) == 1)
{
size++;
}
//System.out.println("Chunk " + getChunkName(magic) + ", size=" + size);
switch(magic)
{
case(MAGIC_BMHD): // main header with width, height, bit depth
{
if (hasBMHD)
{
throw new InvalidFileStructureException("Error in " +
"IFF file: more than one BMHD chunk.");
}
if (size != SIZE_BMHD)
{
throw new InvalidFileStructureException("Cannot " +
"load image. The bitmap header chunk does not " +
"have the expected size.");
}
// image resolution in pixels
width = in.readShort();
height = in.readShort();
if (width < 1 || height < 1)
{
throw new InvalidFileStructureException("Cannot " +
"load image. The IFF file's bitmap header " +
"contains invalid width and height values: " +
width + ", " + height);
}
// next four bytes don't matter
in.skipBytes(4);
// color depth, 1..8 or 24
numPlanes = in.readByte();
if ((numPlanes != 24) && (numPlanes < 1 || numPlanes > 8))
{
throw new UnsupportedTypeException("Cannot load " +
"image, unsupported number of bits per pixel: " +
numPlanes);
}
//System.out.println("\nnum planes=" + numPlanes);
in.readByte(); // discard "masking" value
// compression type, must be 0 or 1
compression = in.readByte();
if (compression != COMPRESSION_NONE &&
compression != COMPRESSION_RLE)
{
throw new UnsupportedTypeException("Cannot load " +
"image, unsupported compression type: " +
compression);
}
//System.out.println(getCompressionName(compression));
in.skipBytes(9);
hasBMHD = true;
break;
}
case(MAGIC_BODY):
{
if (!hasBMHD)
{
// width still has its initialization value -1; no
// bitmap chunk was encountered
throw new InvalidFileStructureException("Cannot load image. Error in " +
"IFF input stream: No bitmap header chunk " +
"encountered before image body chunk.");
}
if (palette == null && (!rgb24))
{
// a missing color map is allowed only for truecolor images
throw new InvalidFileStructureException("Cannot load image. Error in " +
"IFF input stream: No colormap chunk " +
"encountered before image body chunk.");
}
result = loadImage(in);
break;
}
case(MAGIC_CAMG):
{
if (hasCAMG)
{
throw new InvalidFileStructureException("Cannot load image. Error in " +
"IFF input stream: More than one CAMG chunk.");
}
hasCAMG = true;
if (size < 4)
{
throw new InvalidFileStructureException("Cannot load" +
" image. CAMG must be at least four bytes large; " +
"found: " + size);
}
camg = in.readInt();
ham = (camg & 0x800) != 0;
ehb = (camg & 0x80) != 0;
//System.out.println("ham=" + ham);
in.skipBytes(size - 4);
break;
}
case(MAGIC_CMAP): // palette (color map)
{
if (palette != null)
{
throw new InvalidFileStructureException("Cannot " +
"load image. Error in IFF " +
"input stream: More than one palette.");
}
if (size < 3 || (size % 3) != 0)
{
throw new InvalidFileStructureException("Cannot " +
"load image. The size of the colormap is " +
"invalid: " + size);
}
int numColors = size / 3;
palette = new Palette(numColors, 255);
for (int i = 0; i < numColors; i++)
{
palette.putSample(Palette.INDEX_RED, i, in.readByte() & 0xff);
palette.putSample(Palette.INDEX_GREEN, i, in.readByte() & 0xff);
palette.putSample(Palette.INDEX_BLUE, i, in.readByte() & 0xff);
}
break;
}
default:
{
if (in.skipBytes(size) != size)
{
throw new IOException("Error skipping " + size +
" bytes of input stream.");
}
break;
}
}
}
while(result == null);
setImage(result);
}
/**
* Converts input planes to index or truecolor output values.
* Exact interpretation depends on the type of ILBM image storage:
* <ul>
* <li>normal mode; the 1 to 8 planes create index values which are used
* with the colormap</li>
* <li>RGB24; each of the 24 planes adds one bit to the three intensity
* values for red, green and blue; no color map is necessary</li>
* <li>HAM6; a six bit integer (0 to 63) is assembled from the planes
* and the top two bits determine if the previous color is modified or
* if the lower four bits are used as an index into the palette (which
* has consequently 2<sup>4</sup> = 16 entries</li>
* </ul>
* @param sourcePlanes
* @param dest
*/
private void convertRow(byte[][] sourcePlaneData, byte[][] dest)
{
int sourceMask = 0x80;
int sourceIndex = 0;
int lastRed = 0;
int lastGreen = 0;
int lastBlue = 0;
for (int x = 0; x < width; x++)
{
int destMask = 1;
int index = 0;
for (int p = 0; p < sourcePlaneData.length; p++)
{
if ((sourcePlaneData[p][sourceIndex] & sourceMask) != 0)
{
index |= destMask;
}
destMask <<= 1;
}
if ((x & 7) == 7)
{
sourceIndex++;
}
if (sourceMask == 0x01)
{
sourceMask = 0x80;
}
else
{
sourceMask >>= 1;
}
if (ham6)
{
//System.out.println("enter ham6");
int paletteIndex = index & 0x0f;
//System.out.println("palette index=" + paletteIndex);
switch((index >> 4) & 0x03)
{
case(0): // HOLD
{
lastRed = palette.getSample(Palette.INDEX_RED, paletteIndex);
lastGreen = palette.getSample(Palette.INDEX_GREEN, paletteIndex);
lastBlue = palette.getSample(Palette.INDEX_BLUE, paletteIndex);
break;
}
case(1): // MODIFY BLUE
{
lastBlue = (lastBlue & 0x0f) | (paletteIndex << 4);
break;
}
case(2): // MODIFY RED
{
lastRed = (lastRed & 0x0f) | (paletteIndex << 4);
break;
}
case(3): // MODIFY GREEN
{
lastGreen = (lastGreen & 0x0f) | (paletteIndex << 4);
break;
}
}
dest[0][x] = (byte)lastRed;
dest[1][x] = (byte)lastGreen;
dest[2][x] = (byte)lastBlue;
}
else
if (ham8)
{
int paletteIndex = index & 0x3f;
//System.out.println("palette index=" + paletteIndex);
switch((index >> 6) & 0x03)
{
case(0): // HOLD
{
lastRed = palette.getSample(Palette.INDEX_RED, paletteIndex);
lastGreen = palette.getSample(Palette.INDEX_GREEN, paletteIndex);
lastBlue = palette.getSample(Palette.INDEX_BLUE, paletteIndex);
break;
}
case(1): // MODIFY BLUE
{
lastBlue = (lastBlue & 0x03) | (paletteIndex << 2);
break;
}
case(2): // MODIFY RED
{
lastRed = (lastRed & 0x03) | (paletteIndex << 2);
break;
}
case(3): // MODIFY GREEN
{
lastGreen = (lastGreen & 0x03) | (paletteIndex << 2);
break;
}
}
dest[0][x] = (byte)lastRed;
dest[1][x] = (byte)lastGreen;
dest[2][x] = (byte)lastBlue;
}
else
if (rgb24)
{
dest[2][x] = (byte)(index >> 16);
dest[1][x] = (byte)(index >> 8);
dest[0][x] = (byte)index;
}
else
{
/* the value is an index into the lookup table */
//destRgbData[destOffset++] = rgbLookup[index];
dest[0][x] = (byte)index;
}
}
}
private void createExtraHalfbritePalette()
{
if (palette == null)
{
return;
}
int numPaletteEntries = palette.getNumEntries();
Palette tempPalette = new Palette(numPaletteEntries * 2, 255);
for (int i = 0; i < numPaletteEntries; i++)
{
int red = palette.getSample(Palette.INDEX_RED, i);
tempPalette.putSample(Palette.INDEX_RED, numPaletteEntries + i, red);
tempPalette.putSample(Palette.INDEX_RED, i, (red / 2) & 0xf0);
int green = palette.getSample(Palette.INDEX_GREEN, i);
tempPalette.putSample(Palette.INDEX_GREEN, numPaletteEntries + i, red);
tempPalette.putSample(Palette.INDEX_GREEN, i, (green / 2) & 0xf0);
int blue = palette.getSample(Palette.INDEX_BLUE, i);
tempPalette.putSample(Palette.INDEX_BLUE, numPaletteEntries + i, blue);
tempPalette.putSample(Palette.INDEX_BLUE, i, (blue / 2) & 0xf0);
}
palette = tempPalette;
}
private static String getChunkName(int name)
{
StringBuffer sb = new StringBuffer(4);
sb.setLength(4);
sb.setCharAt(0, (char)((name >> 24) & 0xff));
sb.setCharAt(1, (char)((name >> 16) & 0xff));
sb.setCharAt(2, (char)((name >> 8) & 0xff));
sb.setCharAt(3, (char)((name & 0xff)));
return new String(sb);
}
/*private static String getCompressionName(byte method)
{
switch(method)
{
case(COMPRESSION_NONE): return "Uncompressed";
case(COMPRESSION_RLE): return "RLE";
default: return "Unknown method (" + (method & 0xff) + ")";
}
}*/
public String[] getFileExtensions()
{
return new String[] {".lbm", ".iff"};
}
public String getFormatName()
{
return "Amiga Interchange File Format (IFF, LBM)";
}
public String[] getMimeTypes()
{
return new String[] {"image/x-iff"};
}
public boolean isLoadingSupported()
{
return true;
}
public boolean isSavingSupported()
{
return false;
}
/**
* Loads data.length bytes from the input stream to the data array,
* regarding the compression type.
* COMPRESSION_NONE will make this method load data.length bytes from
* the input stream.
* COMPRESSION_RLE will make this method decompress data.length bytes
* from input.
*/
private void loadBytes(DataInput in, byte[] data, int num, int y) throws
InvalidFileStructureException,
IOException
{
switch(compression)
{
case(COMPRESSION_NONE):
{
in.readFully(data, 0, num);
break;
}
case(COMPRESSION_RLE):
{
int x = 0;
while (x < num)
{
int n = in.readByte() & 0xff;
//System.out.println("value=" + n);
boolean compressed = false;
int count = -1;
try
{
if (n < 128)
{
// copy next n + 1 bytes literally
n++;
in.readFully(data, x, n);
x += n;
}
else
{
// if n == -128, nothing happens
if (n > 128)
{
compressed = true;
// otherwise, compute counter
count = 257 - n;
// read another byte
byte value = in.readByte();
// write this byte counter times to output
while (count-- > 0)
{
data[x++] = value;
}
}
}
}
catch (ArrayIndexOutOfBoundsException ioobe)
{
//System.out.println("Loading error");
/* if the encoder did anything wrong, the above code
could potentially write beyond array boundaries
(e.g. if runs of data exceed line boundaries);
this would result in an ArrayIndexOutOfBoundsException
thrown by the virtual machine;
to give a more understandable error message to the
user, this exception is caught here and a
explanatory InvalidFileStructureException is thrown */
throw new InvalidFileStructureException("Error: " +
"RLE-compressed image " +
"file seems to be corrupt (compressed=" + compressed +
", x=" + x + ", y=" + y +
", count=" + (compressed ? (-((int)n) + 1) : n) +
", array length=" + data.length + ").");
}
}
break;
}
default:
{
throw new InvalidFileStructureException("Error loading " +
"image; unknown compression type (" + compression + ")");
}
}
}
/**
* Loads an image from given input stream in, regarding the compression
* type. The image will have 1 to 8 or 24 planes, a resolution given by
* the dimension width times height. The color map data will be used to
* convert index values to RGB pixels.
* Returns the resulting image.
* Will throw an IOException if either there were errors reading from the
* input stream or if the file does not exactly match the file format.
*/
private PixelImage loadImage(DataInput in) throws
InvalidFileStructureException,
IOException,
UnsupportedTypeException,
WrongParameterException
{
setBoundsIfNecessary(width, height);
checkImageResolution();
if (ham)
{
if (numPlanes == 6)
{
ham6 = true;
}
else
if (numPlanes == 8)
{
ham8 = true;
}
else
{
throw new UnsupportedTypeException("Cannot handle " +
"IFF ILBM HAM image file with number of planes " +
"other than 6 or 8 (got " + numPlanes + ").");
}
if (palette == null)
{
throw new InvalidFileStructureException("Invalid IFF ILBM " +
"file: HAM (Hold And Modify) image without a palette.");
}
int numPaletteEntries = palette.getNumEntries();
if (ham6 && numPaletteEntries < 16)
{
throw new InvalidFileStructureException("Invalid IFF ILBM " +
"file: HAM (Hold And Modify) 6 bit image with a " +
"number of palette entries less than 16 (" +
numPaletteEntries + ").");
}
if (ham8 && numPaletteEntries < 64)
{
throw new InvalidFileStructureException("Invalid IFF ILBM " +
"file: HAM (Hold And Modify) 8 bit image with a " +
"number of palette entries less than 64 (" +
numPaletteEntries + ").");
}
}
if (ehb)
{
createExtraHalfbritePalette();
}
int numBytesPerPlane = (width + 7) / 8;
PixelImage image = null;
Paletted8Image palettedImage = null;
RGB24Image rgbImage = null;
if (numPlanes == 24 || ham)
{
rgbImage = new MemoryRGB24Image(getBoundsWidth(), getBoundsHeight());
image = rgbImage;
}
else
{
palettedImage = new MemoryPaletted8Image(getBoundsWidth(), getBoundsHeight(), palette);
image = palettedImage;
}
/* only matters for uncompressed files;
will be true if the number of bytes is odd;
is computed differently for PBM and ILBM types
*/
boolean oddBytesPerRow = (((numBytesPerPlane * numPlanes) % 2) != 0);
if (type == MAGIC_PBM)
{
oddBytesPerRow = ((width % 2) == 1);
}
// plane data will have numPlanes planes for ILBM and 1 plane for PBM
byte[][] planes = null;
int numChannels = 1;
if (type == MAGIC_ILBM)
{
int allocBytes = numBytesPerPlane;
if ((numBytesPerPlane % 2) == 1)
{
allocBytes++;
}
// allocate numPlanes byte arrays
planes = new byte[numPlanes][];
if (rgb24 || ham)
{
numChannels = 3;
}
// for each of these byte arrays allocate numBytesPerPlane bytes
for (int i = 0; i < numPlanes; i++)
{
planes[i] = new byte[allocBytes];
}
}
else
{
// only one plane, but each plane has width bytes instead of
// numBytesPerPlane
planes = new byte[1][];
planes[0] = new byte[width];
}
byte[][] dest = new byte[numChannels][];
for (int i = 0; i < numChannels; i++)
{
dest[i] = new byte[width];
}
for (int y = 0, destY = 0 - getBoundsY1(); y <= getBoundsY2(); y++, destY++)
{
// load one row, different approach for PBM and ILBM
if (type == MAGIC_ILBM)
{
// decode all planes for a complete row
for (int p = 0; p < numPlanes; p++)
{
loadBytes(in, planes[p], numBytesPerPlane, y);
}
}
else
if (type == MAGIC_PBM)
{
loadBytes(in, planes[0], numBytesPerPlane, y);
}
/* all uncompressed rows must have an even number of bytes
so in case the number of bytes per row is odd, one byte
is read and dropped */
if (compression == COMPRESSION_NONE && oddBytesPerRow)
{
in.readByte();
}
setProgress(y, getBoundsY2() + 1);
// if we do not need the row we just loaded we continue loading
// the next row
if (!isRowRequired(y))
{
continue;
}
//System.out.println("storing row " + y + " as " + destY + ", numPlanes="+ numPlanes + ",type=" + type);
// compute offset into pixel data array
if (type == MAGIC_ILBM)
{
convertRow(planes, dest);
if (rgb24 || ham)
{
rgbImage.putByteSamples(RGB24Image.INDEX_RED, 0, destY,
getBoundsWidth(), 1, dest[0], getBoundsX1());
rgbImage.putByteSamples(RGB24Image.INDEX_GREEN, 0, destY,
getBoundsWidth(), 1, dest[1], getBoundsX1());
rgbImage.putByteSamples(RGB24Image.INDEX_BLUE, 0, destY,
getBoundsWidth(), 1, dest[2], getBoundsX1());
}
else
{
palettedImage.putByteSamples(0, 0, destY,
getBoundsWidth(), 1, dest[0], getBoundsX1());
}
}
else
if (type == MAGIC_PBM)
{
palettedImage.putByteSamples(0, 0, destY, getBoundsWidth(), 1,
planes[0], getBoundsX1());
}
}
return image;
}
public void process() throws
InvalidFileStructureException,
MissingParameterException,
OperationFailedException,
UnsupportedTypeException,
WrongFileFormatException
{
initModeFromIOObjects();
if (getMode() == CodecMode.LOAD)
{
try
{
checkAndLoad();
}
catch (IOException ioe)
{
throw new InvalidFileStructureException("I/O error while loading: " + ioe.toString());
}
}
else
{
throw new OperationFailedException("Only loading from IFF is supported.");
}
}
}