Fixed numerous long-standing bugs in the IFFImageReader and IFFImageWriter.

- Fixed EOF bug for RLE compressed BODY chunks with padding
 - Fixed alignment bug for rows (now 16 bit aligned/padded).
 - Added a couple of TODOs for more known bugs...
 - Some general clean-up.
This commit is contained in:
Harald Kuhr 2009-11-04 01:27:56 +01:00
parent c801926a02
commit 96a74e0b81
5 changed files with 62 additions and 45 deletions

View File

@ -154,7 +154,7 @@ public class IFFImageReader extends ImageReaderBase {
remaining -= 8; remaining -= 8;
remaining -= length % 2 == 0 ? length : length + 1; remaining -= length % 2 == 0 ? length : length + 1;
//System.out.println("Next chunk: " + toChunkStr(chunkId) + " lenght: " + length); //System.out.println("Next chunk: " + toChunkStr(chunkId) + " length: " + length);
//System.out.println("Remaining bytes after chunk: " + remaining); //System.out.println("Remaining bytes after chunk: " + remaining);
switch (chunkId) { switch (chunkId) {
@ -232,6 +232,7 @@ public class IFFImageReader extends ImageReaderBase {
readBody(pParam); readBody(pParam);
} }
else { else {
// TODO: Remove this hack when we have metadata
// In the rare case of an ILBM containing nothing but a CMAP // In the rare case of an ILBM containing nothing but a CMAP
//System.out.println(mColorMap); //System.out.println(mColorMap);
if (mColorMap != null) { if (mColorMap != null) {
@ -244,7 +245,6 @@ public class IFFImageReader extends ImageReaderBase {
processImageComplete(); processImageComplete();
return result; return result;
} }
@ -294,6 +294,7 @@ public class IFFImageReader extends ImageReaderBase {
// 8 bit // 8 bit
// May be HAM8 // May be HAM8
if (!isHAM()) { if (!isHAM()) {
// TODO: This is probably a bug, as mColorMap is null in case of gray..
IndexColorModel cm = mColorMap.getIndexColorModel(); IndexColorModel cm = mColorMap.getIndexColorModel();
if (cm != null) { if (cm != null) {
specifier = IndexedImageTypeSpecifier.createFromIndexColorModel(cm); specifier = IndexedImageTypeSpecifier.createFromIndexColorModel(cm);
@ -304,6 +305,7 @@ public class IFFImageReader extends ImageReaderBase {
break; break;
} }
} }
// NOTE: HAM modes falls through, as they are converted to RGB
case 24: case 24:
// 24 bit RGB // 24 bit RGB
specifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR); specifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR);
@ -313,12 +315,12 @@ public class IFFImageReader extends ImageReaderBase {
specifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR); specifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR);
break; break;
default: default:
throw new IIOException("Bit depth not implemented: " + mHeader.mBitplanes); throw new IIOException(String.format("Bit depth not implemented: %d", mHeader.mBitplanes));
} }
return specifier; return specifier;
} }
private void readBody(ImageReadParam pParam) throws IOException { private void readBody(final ImageReadParam pParam) throws IOException {
mImageInput.seek(mBodyStart); // 8 for the header before length in stream mImageInput.seek(mBodyStart); // 8 for the header before length in stream
mByteRunStream = null; mByteRunStream = null;
@ -333,7 +335,7 @@ public class IFFImageReader extends ImageReaderBase {
} }
private void readIndexed(ImageReadParam pParam, final ImageInputStream pInput, final IndexColorModel pModel) throws IOException { private void readIndexed(final ImageReadParam pParam, final ImageInputStream pInput, final IndexColorModel pModel) throws IOException {
final int width = mHeader.mWidth; final int width = mHeader.mWidth;
final int height = mHeader.mHeight; final int height = mHeader.mHeight;
@ -363,7 +365,9 @@ public class IFFImageReader extends ImageReaderBase {
destination = destination.createWritableChild(0, 0, destination.getWidth(), destination.getHeight(), offset.x, offset.y, destinationBands); destination = destination.createWritableChild(0, 0, destination.getWidth(), destination.getHeight(), offset.x, offset.y, destinationBands);
} }
int planeWidth = (width + 7) / 8; // NOTE: Each row of the image is stored in an integral number of 16 bit words.
// The number of words per row is words=((w+15)/16)
int planeWidth = 2 * ((width + 15) / 16);
final byte[] planeData = new byte[8 * planeWidth]; final byte[] planeData = new byte[8 * planeWidth];
ColorModel cm; ColorModel cm;
@ -373,10 +377,14 @@ public class IFFImageReader extends ImageReaderBase {
// TODO: If HAM6, use type USHORT_444_RGB or 2BYTE_444_RGB? // TODO: If HAM6, use type USHORT_444_RGB or 2BYTE_444_RGB?
// Or create a HAMColorModel, if at all possible? // Or create a HAMColorModel, if at all possible?
// TYPE_3BYTE_BGR // TYPE_3BYTE_BGR
cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[]{8, 8, 8}, cm = new ComponentColorModel(
false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[]{8, 8, 8},
false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE
);
// Create a byte raster with BGR order // Create a byte raster with BGR order
raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, 1, width * 3, 3, new int[]{2, 1, 0}, null); raster = Raster.createInterleavedRaster(
DataBuffer.TYPE_BYTE, width, 1, width * 3, 3, new int[]{2, 1, 0}, null
);
} }
else { else {
// TYPE_BYTE_BINARY or TYPE_BYTE_INDEXED // TYPE_BYTE_BINARY or TYPE_BYTE_INDEXED
@ -400,15 +408,9 @@ public class IFFImageReader extends ImageReaderBase {
ColorConvertOp converter = null; ColorConvertOp converter = null;
for (int srcY = 0; srcY < height; srcY++) { for (int srcY = 0; srcY < height; srcY++) {
for (int p = 0; p < planes; p++) { for (int p = 0; p < planes; p++) {
try {
readPlaneData(pInput, planeData, p * planeWidth, planeWidth); readPlaneData(pInput, planeData, p * planeWidth, planeWidth);
} }
catch (IOException e) {
// TODO: Add warning? Probbably a bug somewhere, should not catch!
}
}
// Skip rows outside AOI // Skip rows outside AOI
if (srcY < aoi.y || (srcY - aoi.y) % sourceYSubsampling != 0) { if (srcY < aoi.y || (srcY - aoi.y) % sourceYSubsampling != 0) {
@ -420,11 +422,9 @@ public class IFFImageReader extends ImageReaderBase {
if (mFormType == IFF.TYPE_ILBM) { if (mFormType == IFF.TYPE_ILBM) {
int pixelPos = 0; int pixelPos = 0;
int planePos = 0; for (int planePos = 0; planePos < planeWidth; planePos++) {
for (int i = 0; i < planeWidth; i++) {
IFFUtil.bitRotateCW(planeData, planePos, planeWidth, row, pixelPos, 1); IFFUtil.bitRotateCW(planeData, planePos, planeWidth, row, pixelPos, 1);
pixelPos += 8; pixelPos += 8;
planePos++;
} }
if (isHAM()) { if (isHAM()) {
@ -434,11 +434,14 @@ public class IFFImageReader extends ImageReaderBase {
raster.setDataElements(0, 0, width, 1, row); raster.setDataElements(0, 0, width, 1, row);
} }
} }
else /*if (mType == IFFImageReader.TYPE_PBM)*/ { else if (mFormType == IFF.TYPE_PBM) {
// TODO: Arraycopy might not be neccessary, if it's okay with row larger than width // TODO: Arraycopy might not be necessary, if it's okay with row larger than width
System.arraycopy(planeData, 0, row, 0, mHeader.mBitplanes * planeWidth); System.arraycopy(planeData, 0, row, 0, mHeader.mBitplanes * planeWidth);
raster.setDataElements(0, 0, width, 1, row); raster.setDataElements(0, 0, width, 1, row);
} }
else {
throw new AssertionError(String.format("Unsupported FORM type: %s", mFormType));
}
int dstY = (srcY - aoi.y) / sourceYSubsampling; int dstY = (srcY - aoi.y) / sourceYSubsampling;
// Handle non-converting raster as special case for performance // Handle non-converting raster as special case for performance
@ -521,8 +524,9 @@ public class IFFImageReader extends ImageReaderBase {
// Ensure band settings from param are compatible with images // Ensure band settings from param are compatible with images
checkReadParamBandSettings(pParam, mHeader.mBitplanes / 8, mImage.getSampleModel().getNumBands()); checkReadParamBandSettings(pParam, mHeader.mBitplanes / 8, mImage.getSampleModel().getNumBands());
int planeWidth = (width + 7) / 8; // NOTE: Each row of the image is stored in an integral number of 16 bit words.
// The number of words per row is words=((w+15)/16)
int planeWidth = 2 * ((width + 15) / 16);
final byte[] planeData = new byte[8 * planeWidth]; final byte[] planeData = new byte[8 * planeWidth];
WritableRaster destination = mImage.getRaster(); WritableRaster destination = mImage.getRaster();
@ -558,16 +562,17 @@ public class IFFImageReader extends ImageReaderBase {
int off = (channels - c - 1); int off = (channels - c - 1);
int pixelPos = 0; int pixelPos = 0;
int planePos = 0; for (int planePos = 0; planePos < planeWidth; planePos++) {
for (int i = 0; i < planeWidth; i++) {
IFFUtil.bitRotateCW(planeData, planePos, planeWidth, data, off + pixelPos * channels, channels); IFFUtil.bitRotateCW(planeData, planePos, planeWidth, data, off + pixelPos * channels, channels);
pixelPos += 8; pixelPos += 8;
planePos++;
} }
} }
else /*if (mType == IFFImageReader.TYPE_PBM)*/ { else if (mFormType == IFF.TYPE_PBM) {
System.arraycopy(planeData, 0, data, srcY * 8 * planeWidth, planeWidth); System.arraycopy(planeData, 0, data, srcY * 8 * planeWidth, planeWidth);
} }
else {
throw new AssertionError(String.format("Unsupported FORM type: %s", mFormType));
}
} }
int dstY = (srcY - aoi.y) / sourceYSubsampling; int dstY = (srcY - aoi.y) / sourceYSubsampling;
@ -607,16 +612,25 @@ public class IFFImageReader extends ImageReaderBase {
break; break;
case BMHDChunk.COMPRESSION_BYTE_RUN: case BMHDChunk.COMPRESSION_BYTE_RUN:
// TODO: How do we know if the last byte in the body is a pad byte or not?!
// The body consists of byte-run (PackBits) compressed rows of bit plane data.
// However, we don't know how long each compressed row is, without decoding it...
// The workaround below, is to use a decode buffer size of pPlaneWidth,
// to make sure we don't decode anything we don't have to (shouldn't).
if (mByteRunStream == null) { if (mByteRunStream == null) {
mByteRunStream = new DataInputStream(new DecoderStream( mByteRunStream = new DataInputStream(
new DecoderStream(
IIOUtil.createStreamAdapter(pInput, mBody.mChunkLength), IIOUtil.createStreamAdapter(pInput, mBody.mChunkLength),
new PackBitsDecoder(true) new PackBitsDecoder(true),
)); pPlaneWidth * mHeader.mBitplanes
)
);
} }
mByteRunStream.readFully(pData, pOffset, pPlaneWidth); mByteRunStream.readFully(pData, pOffset, pPlaneWidth);
break; break;
default: default:
throw new IIOException("Unknown compression type: " + mHeader.mCompressionType); throw new IIOException(String.format("Unknown compression type: %d", mHeader.mCompressionType));
} }
} }
@ -666,7 +680,7 @@ public class IFFImageReader extends ImageReaderBase {
} }
public static void main(String[] pArgs) throws IOException { public static void main(String[] pArgs) throws IOException {
ImageReader reader = new IFFImageReader(new IFFImageReaderSpi()); ImageReader reader = new IFFImageReader();
// ImageInputStream input = ImageIO.createImageInputStream(new File(pArgs[0])); // ImageInputStream input = ImageIO.createImageInputStream(new File(pArgs[0]));
ImageInputStream input = new BufferedImageInputStream(ImageIO.createImageInputStream(new File(pArgs[0]))); ImageInputStream input = new BufferedImageInputStream(ImageIO.createImageInputStream(new File(pArgs[0])));
@ -687,7 +701,7 @@ public class IFFImageReader extends ImageReaderBase {
BufferedImage image = reader.read(0, param); BufferedImage image = reader.read(0, param);
System.out.println("image = " + image); System.out.println("image = " + image);
showIt(image, ""); showIt(image, pArgs[0]);
} }
} }
} }

View File

@ -47,7 +47,7 @@ import java.io.OutputStream;
/** /**
* Writer for Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) format. * Writer for Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) format.
* The IFF format (Interchange File Format) is the standard file format * The IFF format (Interchange File Format) is the standard file format
* supported by allmost all image software for the Amiga computer. * supported by almost all image software for the Amiga computer.
* <p/> * <p/>
* *
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a> * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
@ -58,8 +58,6 @@ import java.io.OutputStream;
*/ */
public class IFFImageWriter extends ImageWriterBase { public class IFFImageWriter extends ImageWriterBase {
private static final byte[] ANNO_DATA = "Written by TwelveMonkeys IFFImageWriter 1.0 for Java (javax.imageio).".getBytes();
public IFFImageWriter() { public IFFImageWriter() {
this(null); this(null);
} }
@ -121,6 +119,7 @@ public class IFFImageWriter extends ImageWriterBase {
private void packImageData(OutputStream pOutput, RenderedImage pImage, ImageWriteParam pParam) throws IOException { private void packImageData(OutputStream pOutput, RenderedImage pImage, ImageWriteParam pParam) throws IOException {
// TODO: Allow param to dictate uncompressed // TODO: Allow param to dictate uncompressed
// TODO: Allow param to dictate type PBM?
// TODO: Subsample/AOI // TODO: Subsample/AOI
final boolean compress = shouldCompress(pImage); final boolean compress = shouldCompress(pImage);
final OutputStream output = compress ? new EncoderStream(pOutput, new PackBitsEncoder(), true) : pOutput; final OutputStream output = compress ? new EncoderStream(pOutput, new PackBitsEncoder(), true) : pOutput;
@ -136,12 +135,14 @@ public class IFFImageWriter extends ImageWriterBase {
// 2. Perform byteRun1 compression for each plane separately // 2. Perform byteRun1 compression for each plane separately
// 3. Write the plane data for each plane // 3. Write the plane data for each plane
final int planeWidth = (width + 7) / 8; //final int planeWidth = (width + 7) / 8;
final int planeWidth = 2 * ((width + 15) / 16);
final byte[] planeData = new byte[8 * planeWidth]; final byte[] planeData = new byte[8 * planeWidth];
final int channels = (model.getPixelSize() + 7) / 8; final int channels = (model.getPixelSize() + 7) / 8;
final int planesPerChannel = channels == 1 ? model.getPixelSize() : 8; final int planesPerChannel = channels == 1 ? model.getPixelSize() : 8;
int[] pixels = new int[8 * planeWidth]; int[] pixels = new int[8 * planeWidth];
// TODO: The spec says "Do not compress across rows!".. I think we currently do.
// NOTE: I'm a little unsure if this is correct for 4 channel (RGBA) // NOTE: I'm a little unsure if this is correct for 4 channel (RGBA)
// data, but it is at least consistent with the IFFImageReader for now... // data, but it is at least consistent with the IFFImageReader for now...
for (int y = 0; y < height; y++) { for (int y = 0; y < height; y++) {
@ -174,7 +175,8 @@ public class IFFImageWriter extends ImageWriterBase {
private void writeMeta(RenderedImage pImage, int pBodyLength) throws IOException { private void writeMeta(RenderedImage pImage, int pBodyLength) throws IOException {
// Annotation ANNO chunk, 8 + annoData.length bytes // Annotation ANNO chunk, 8 + annoData.length bytes
GenericChunk anno = new GenericChunk(IFFUtil.toInt("ANNO".getBytes()), ANNO_DATA); String annotation = "Written by " + getOriginatingProvider().getDescription(null);
GenericChunk anno = new GenericChunk(IFFUtil.toInt("ANNO".getBytes()), annotation.getBytes());
ColorModel cm = pImage.getColorModel(); ColorModel cm = pImage.getColorModel();
IndexColorModel icm = null; IndexColorModel icm = null;

View File

@ -69,8 +69,9 @@ public class IFFImageWriterSpi extends ImageWriterSpi {
); );
} }
public boolean canEncodeImage(ImageTypeSpecifier pType) { public boolean canEncodeImage(final ImageTypeSpecifier pType) {
// TODO: Probably can't store 16 bit types etc... // TODO: Probably can't store 16 bit types etc...
// TODO: Can't store CMYK (well.. it does, but they can't be read back)
return true; return true;
} }

View File

@ -48,7 +48,7 @@ class IFFUtil {
/** /**
* Creates a rotation table * Creates a rotation table
* @param n * @param n number of bits -1
* *
* @return the rotation table * @return the rotation table
*/ */
@ -243,7 +243,7 @@ class IFFUtil {
/** /**
* Converts an int to a four letter String. * Converts an int to a four letter String.
* *
* @param pChunkId * @param pChunkId the chunk identifier
* @return a String * @return a String
*/ */
static String toChunkStr(int pChunkId) { static String toChunkStr(int pChunkId) {

View File

@ -52,13 +52,13 @@ public class IFFImageReaderTestCase extends ImageReaderAbstractTestCase<IFFImage
new TestData(getClassLoaderResource("/iff/survivor.iff"), new Dimension(800, 600)), // 24 bit new TestData(getClassLoaderResource("/iff/survivor.iff"), new Dimension(800, 600)), // 24 bit
// HAM6 - Ok (a lot of visual "fringe", would be interesting to see on a real HAM display) // HAM6 - Ok (a lot of visual "fringe", would be interesting to see on a real HAM display)
new TestData(getClassLoaderResource("/iff/A4000T_HAM6.IFF"), new Dimension(320, 512)), // ham6 new TestData(getClassLoaderResource("/iff/A4000T_HAM6.IFF"), new Dimension(320, 512)), // ham6
// HAM8 - Passes tests, but visuals are trashed. Have other HAM8 files that are ok // HAM8 - Ok (PackBits decoder chokes on padding byte)
new TestData(getClassLoaderResource("/iff/A4000T_HAM8.IFF"), new Dimension(628, 512)), // ham8 new TestData(getClassLoaderResource("/iff/A4000T_HAM8.IFF"), new Dimension(628, 512)), // ham8
// 8 color indexed - Passes tests, but trashed. Must be something special with these images // 8 color indexed - Ok
new TestData(getClassLoaderResource("/iff/AmigaBig.iff"), new Dimension(300, 200)), // 8 color new TestData(getClassLoaderResource("/iff/AmigaBig.iff"), new Dimension(300, 200)), // 8 color
// 8 color indexed - Ok // 8 color indexed - Ok
new TestData(getClassLoaderResource("/iff/AmigaAmiga.iff"), new Dimension(200, 150)), // 8 color new TestData(getClassLoaderResource("/iff/AmigaAmiga.iff"), new Dimension(200, 150)), // 8 color
// Breaks completely... Could be bug in the packbits decoder? // Ok (PackBits decoder chokes on padding byte)
new TestData(getClassLoaderResource("/iff/Abyss.iff"), new Dimension(320, 400)) new TestData(getClassLoaderResource("/iff/Abyss.iff"), new Dimension(320, 400))
); );
} }