IFFImageReader.java

/*
 * Copyright (c) 2008, Harald Kuhr
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

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;
import com.twelvemonkeys.io.enc.PackBitsDecoder;

import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.color.*;
import java.awt.image.*;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import static com.twelvemonkeys.imageio.plugins.iff.IFFUtil.toChunkStr;

/**
 * Reader for Commodore Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) and PBM
 * format (Packed BitMap). Also supports IFF RGB8 (Impulse) and IFF DEEP (TVPaint).
 * The IFF format (Interchange File Format) is the standard file format
 * supported by allmost all image software for the Amiga computer.
 * <p>
 * This reader supports the original palette-based 1-8 bit formats, including
 * EHB (Extra Half-Bright), HAM (Hold and Modify), and the more recent "deep"
 * formats, 8 bit gray, 24 bit RGB and 32 bit ARGB.
 * Uncompressed and ByteRun1 compressed (run length encoding) files are
 * supported.
 * </p>
 * <p>
 * Palette based images are read as {@code BufferedImage} of
 * {@link BufferedImage#TYPE_BYTE_INDEXED TYPE_BYTE_INDEXED} or
 * {@link BufferedImage#TYPE_BYTE_BINARY BufferedImage#}
 * depending on the bit depth.
 * Gray images are read as
 * {@link BufferedImage#TYPE_BYTE_GRAY TYPE_BYTE_GRAY}.
 * 24 bit true-color images are read as
 * {@link BufferedImage#TYPE_3BYTE_BGR TYPE_3BYTE_BGR}.
 * 32 bit true-color images are read as
 * {@link BufferedImage#TYPE_4BYTE_ABGR TYPE_4BYTE_ABGR}.
 * </p>
 * <p>
 * Issues: HAM and HAM8 (Hold and Modify) formats are converted to RGB (24 bit),
 * as it seems to be very hard to create an {@code IndexColorModel} subclass
 * that would correctly describe these formats.
 * These formats utilizes the special display hardware in the Amiga computers.
 * HAM (6 bits) needs 12 bits storage/pixel, if unpacked to RGB (4 bits/gun).
 * HAM8 (8 bits) needs 18 bits storage/pixel, if unpacked to RGB (6 bits/gun).
 * See <a href="http://en.wikipedia.org/wiki/Hold_And_Modify">Wikipedia: HAM</a>
 * for more information.
 * <br>
 * EHB palette is expanded to an {@link IndexColorModel} with 64 entries.
 * See <a href="http://en.wikipedia.org/wiki/Extra_Half-Brite">Wikipedia: EHB</a>
 * for more information.
 * </p>
 *
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @author last modified by $Author: haku $
 * @version $Id: IFFImageReader.java,v 1.0 29.aug.2004 20:26:58 haku Exp $
 * @see <a href="http://en.wikipedia.org/wiki/Interchange_File_Format">Wikipedia: IFF</a>
 * @see <a href="http://en.wikipedia.org/wiki/ILBM">Wikipedia: IFF ILBM</a>
 */
public final class IFFImageReader extends ImageReaderBase {
    // http://home.comcast.net/~erniew/lwsdk/docs/filefmts/ilbm.html
    // http://www.fileformat.info/format/iff/spec/7866a9f0e53c42309af667c5da3bd426/view.htm
    //   - Contains definitions of some "new" chunks, as well as alternative FORM types
    // http://amigan.1emu.net/index/iff.html

    // TODO: Allow reading rasters for HAM6/HAM8 and multipalette images that are expanded to RGB (24 bit) during read.

    final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.iff.debug"));

    private Form header;
    private DataInputStream byteRunStream;

    IFFImageReader(ImageReaderSpi pProvider) {
        super(pProvider);
    }

    private void init(int pIndex) throws IOException {
        checkBounds(pIndex);

        if (header == null) {
            readMeta();
        }
    }

    @Override
    protected void resetMembers() {
        header = null;
        byteRunStream = null;
    }

    private void readMeta() throws IOException {
        int chunkType = imageInput.readInt();
        if (chunkType != IFF.CHUNK_FORM) {
            throw new IIOException(String.format("Unknown file format for IFFImageReader, expected 'FORM': %s", toChunkStr(chunkType)));
        }

        int remaining = imageInput.readInt() - 4; // We'll read 4 more in a sec

        int formType = imageInput.readInt();
        if (formType != IFF.TYPE_ILBM && formType != IFF.TYPE_PBM && formType != IFF.TYPE_RGB8 && formType != IFF.TYPE_DEEP && formType != IFF.TYPE_TVPP) {
            throw new IIOException(String.format("Only IFF FORM types 'ILBM' and 'PBM ' supported: %s", toChunkStr(formType)));
        }

        if (DEBUG) {
            System.out.println("IFF type FORM '" + toChunkStr(formType) + "', len: " + (remaining + 4));
            System.out.println("Reading Chunks...");
        }

        header = Form.ofType(formType);

        // TODO: Delegate the FORM reading to the Form class or a FormReader class?
        while (remaining > 0) {
            int chunkId = imageInput.readInt();
            int length = imageInput.readInt();

            remaining -= 8;
            remaining -= length % 2 == 0 ? length : length + 1;

            if (DEBUG) {
                System.out.println("Next chunk: " + toChunkStr(chunkId) + " @ pos: " + (imageInput.getStreamPosition() - 8) + ", len: " + length);
                System.out.println("Remaining bytes after chunk: " + remaining);
            }

            switch (chunkId) {
                case IFF.CHUNK_BMHD:
                    BMHDChunk bitmapHeader = new BMHDChunk(length);
                    bitmapHeader.readChunk(imageInput);
                    header = header.with(bitmapHeader);
                    break;

                case IFF.CHUNK_DGBL:
                    DGBLChunk deepGlobal = new DGBLChunk(length);
                    deepGlobal.readChunk(imageInput);
                    header = header.with(deepGlobal);
                    break;

                case IFF.CHUNK_DLOC:
                    DLOCChunk deepLocation = new DLOCChunk(length);
                    deepLocation.readChunk(imageInput);
                    header = header.with(deepLocation);
                    break;

                case IFF.CHUNK_DPEL:
                    DPELChunk deepPixel = new DPELChunk(length);
                    deepPixel.readChunk(imageInput);
                    header = header.with(deepPixel);
                    break;

                case IFF.CHUNK_XS24:
                    XS24Chunk thumbnail = new XS24Chunk(length);
                    thumbnail.readChunk(imageInput);
                    header = header.with(thumbnail);
                    break;

                case IFF.CHUNK_CMAP:
                    CMAPChunk colorMap = new CMAPChunk(length);
                    colorMap.readChunk(imageInput);
                    header = header.with(colorMap);
                    break;

                case IFF.CHUNK_GRAB:
                    GRABChunk grab = new GRABChunk(length);
                    grab.readChunk(imageInput);
                    header = header.with(grab);
                    break;

                case IFF.CHUNK_CAMG:
                    CAMGChunk viewMode = new CAMGChunk(length);
                    viewMode.readChunk(imageInput);
                    header = header.with(viewMode);
                    break;

                case IFF.CHUNK_PCHG:
                    PCHGChunk pchg = new PCHGChunk(length);
                    pchg.readChunk(imageInput);
                    header = header.with(pchg);
                    break;

                case IFF.CHUNK_SHAM:
                    SHAMChunk sham = new SHAMChunk(length);
                    sham.readChunk(imageInput);
                    header = header.with(sham);
                    break;

                case IFF.CHUNK_CTBL:
                    CTBLChunk ctbl = new CTBLChunk(length);
                    ctbl.readChunk(imageInput);
                    header = header.with(ctbl);
                    break;

                case IFF.CHUNK_BODY:
                case IFF.CHUNK_DBOD:
                    // NOTE: We don't read the body here, it's done later in the read(int, ImageReadParam) method
                    BODYChunk body = new BODYChunk(chunkId, length, imageInput.getStreamPosition());
                    header = header.with(body);

                    // Done reading meta
                    if (DEBUG) {
                        System.out.println("header = " + header);
                    }
                    return;

                case IFF.CHUNK_ANNO:
                case IFF.CHUNK_AUTH:
                case IFF.CHUNK_COPY:
                case IFF.CHUNK_NAME:
                case IFF.CHUNK_TEXT:
                case IFF.CHUNK_UTF8:
                    GenericChunk generic = new GenericChunk(chunkId, length);
                    generic.readChunk(imageInput);
                    header = header.with(generic);
                    break;

                case IFF.CHUNK_JUNK:
                    // Always skip junk chunks
                default:
                    // TODO: DEST, SPRT and more
                    // Everything else, we'll just skip
                    IFFChunk.skipData(imageInput, length, 0);
                    break;
            }
        }

        if (DEBUG) {
            System.out.println("header = " + header);
            System.out.println("No BODY chunk found...");
        }
    }

    @Override
    public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
        init(imageIndex);
        processImageStarted(imageIndex);

        BufferedImage result = getDestination(param, getImageTypes(imageIndex), getWidth(imageIndex), getHeight(imageIndex));
        readBody(param, result);

        processImageComplete();

        return result;
    }

    @Override
    public boolean readerSupportsThumbnails() {
        return true;
    }

    @Override
    public boolean hasThumbnails(final int imageIndex) throws IOException {
        init(imageIndex);
        return header.hasThumbnail();
    }

    @Override
    public int getNumThumbnails(final int imageIndex) throws IOException {
        init(imageIndex);
        return header.hasThumbnail() ? 1 : 0;
    }

    @Override
    public int getThumbnailWidth(final int imageIndex, final int thumbnailIndex) throws IOException {
        init(imageIndex);
        if (!header.hasThumbnail() || thumbnailIndex > 1) {
            throw new IndexOutOfBoundsException("thumbnailIndex out of bounds: " + thumbnailIndex);
        }

        return header.thumbnailWidth();
    }

    @Override
    public int getThumbnailHeight(final int imageIndex, final int thumbnailIndex) throws IOException {
        init(imageIndex);
        if (!header.hasThumbnail() || thumbnailIndex > 1) {
            throw new IndexOutOfBoundsException("thumbnailIndex out of bounds: " + thumbnailIndex);
        }

        return header.thumbnailHeight();
    }

    @Override
    public BufferedImage readThumbnail(final int imageIndex, final int thumbnailIndex) throws IOException {
        init(imageIndex);
        if (!header.hasThumbnail() || thumbnailIndex > 1) {
            throw new IndexOutOfBoundsException("thumbnailIndex out of bounds: " + thumbnailIndex);
        }

        processThumbnailStarted(imageIndex, thumbnailIndex);

        BufferedImage thumbnail = header.thumbnail();

        processThumbnailProgress(100f);
        processThumbnailComplete();

        return thumbnail;
    }

    @Override
    public int getWidth(int imageIndex) throws IOException {
        init(imageIndex);
        return header.width();
    }

    @Override
    public int getHeight(int imageIndex) throws IOException {
        init(imageIndex);
        return header.height();
    }

    @Override
    public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
        return new IFFImageMetadata(getRawImageType(imageIndex), header, header.colorMap());
    }

    @Override
    public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
        init(imageIndex);

        int bitplanes = header.bitplanes();
        List<ImageTypeSpecifier> types =
                header.formType == IFF.TYPE_DEEP || header.formType == IFF.TYPE_TVPP // TODO: Make a header attribute here
                ? Arrays.asList(
                        ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR_PRE),
                        getRawImageType(imageIndex)
                )
                : Arrays.asList(
                        getRawImageType(imageIndex),
                        ImageTypeSpecifiers.createFromBufferedImageType(bitplanes == 32 ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR)
                );
        // TODO: Allow 32 bit INT types?
        return types.iterator();
    }

    @Override
    public ImageTypeSpecifier getRawImageType(int pIndex) throws IOException {
        init(pIndex);

        // NOTE: colorMap may be null for 8 bit (gray), 24 bit or 32 bit only
        switch (header.bitplanes()) {
            case 1:
                // -> 1 bit IndexColorModel
            case 2:
                // -> 2 bit IndexColorModel
            case 3:
            case 4:
                // -> 4 bit IndexColorModel
            case 5:
            case 6:
                // May be EHB or HAM6
            case 7:
            case 8:
                // May be HAM8
                // otherwise -> 8 bit IndexColorModel
                if (!needsConversionToRGB()) {
                    IndexColorModel indexColorModel = header.colorMap();

                    if (indexColorModel != null) {
                        return ImageTypeSpecifiers.createFromIndexColorModel(indexColorModel);
                    }

                    return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY);
                }
                // NOTE: HAM modes falls through, as they are converted to RGB
            case 24:
                // 24 bit RGB
                return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR);

            case 25:
                // For TYPE_RGB8: 24 bit + 1 bit mask (we'll convert to full alpha during decoding)
                if (header.formType != IFF.TYPE_RGB8) {
                    throw new IIOException(String.format("25 bit depth only supported for FORM type RGB8: %s", toChunkStr(header.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
                return header.formType == IFF.TYPE_DEEP || header.formType == IFF.TYPE_TVPP
                       //                                                                                                R  G  B  A
                       ? ImageTypeSpecifiers.createInterleaved(ColorSpaces.getColorSpace(ColorSpace.CS_sRGB), new int[] {1, 2, 3, 0}, DataBuffer.TYPE_BYTE, true, header.premultiplied()) // TODO: Create based on DPEL!
                       : ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR);

            default:
                throw new IIOException(String.format("Bit depth not implemented: %d", header.bitplanes()));
        }
    }

    private boolean needsConversionToRGB() {
        return header.isHAM() || header.isMultiPalette();
    }

    private void readBody(final ImageReadParam param, final BufferedImage destination) throws IOException {
        if (DEBUG) {
            System.out.println("Reading body");
            System.out.println("pos: " + imageInput.getStreamPosition());
            System.out.println("body offset: " + header.bodyOffset());
        }

        imageInput.seek(header.bodyOffset());
        byteRunStream = null;

        if (header.formType == IFF.TYPE_RGB8 || header.formType == IFF.TYPE_DEEP || header.formType == IFF.TYPE_TVPP) {
            readChunky(param, destination, imageInput);
        }
        else if (header.colorMap() != null) {
            // NOTE: For ILBM types, colorMap may be null for 8 bit (gray), 24 bit or 32 bit only
            IndexColorModel palette = header.colorMap();
            readInterleavedIndexed(param, destination, palette, imageInput);
        }
        else {
            readInterleaved(param, destination, imageInput);
        }
    }

    private void readInterleavedIndexed(final ImageReadParam param, final BufferedImage destination, final IndexColorModel palette, final ImageInputStream input) throws IOException {
        final int width = header.width();
        final int height = header.height();

        final Rectangle aoi = getSourceRegion(param, width, height);
        final Point offset = param == null ? new Point(0, 0) : param.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 (param != null) {
            sourceXSubsampling = param.getSourceXSubsampling();
            sourceYSubsampling = param.getSourceYSubsampling();

            sourceBands = param.getSourceBands();
            destinationBands = param.getDestinationBands();
        }

        // Ensure band settings from param are compatible with images
        checkReadParamBandSettings(param, needsConversionToRGB() ? 3 : 1, destination.getSampleModel().getNumBands());

        WritableRaster destRaster = destination.getRaster();
        if (destinationBands != null || offset.x != 0 || offset.y != 0) {
            destRaster = destRaster.createWritableChild(0, 0, destRaster.getWidth(), destRaster.getHeight(), offset.x, offset.y, destinationBands);
        }

        // 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];

        ColorModel cm;
        WritableRaster rowRaster;

        if (needsConversionToRGB()) {
            // TODO: Create a HAMColorModel, if at all possible?
            // TYPE_3BYTE_BGR
            cm = new ComponentColorModel(
                    ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] {8, 8, 8},
                    false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE
            );
            // Create a byte raster with BGR order
            rowRaster = Raster.createInterleavedRaster(
                    DataBuffer.TYPE_BYTE, width, 1, width * 3, 3, new int[] {2, 1, 0}, null
            );
        }
        else {
            // TYPE_BYTE_BINARY or TYPE_BYTE_INDEXED
            cm = palette;
            rowRaster = palette.createCompatibleWritableRaster(width, 1);
        }

        Raster sourceRow = rowRaster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, sourceBands);

        final byte[] row = new byte[width * 8];
        final byte[] data = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
        final int planes = header.bitplanes();

        Object dataElements = null;
        Object outDataElements = null;
        ColorConvertOp converter = null;

        for (int srcY = 0; srcY < height; srcY++) {
            for (int p = 0; p < planes; p++) {
                readPlaneData(planeData, p * planeWidth, planeWidth, input);
            }

            // Skip rows outside AOI
            if (srcY < aoi.y || (srcY - aoi.y) % sourceYSubsampling != 0) {
                continue;
            }
            else if (srcY >= (aoi.y + aoi.height)) {
                return;
            }

            if (header.formType == IFF.TYPE_ILBM) {
                int pixelPos = 0;
                for (int planePos = 0; planePos < planeWidth; planePos++) {
                    IFFUtil.bitRotateCW(planeData, planePos, planeWidth, row, pixelPos, 1);
                    pixelPos += 8;
                }

                if (header.isHAM()) {
                    hamToRGB(row, palette, data, 0);
                }
                else if (needsConversionToRGB()) {
                    multiPaletteToRGB(srcY, row, palette, data, 0);
                }
                else {
                    rowRaster.setDataElements(0, 0, width, 1, row);
                }
            }
            else if (header.formType == IFF.TYPE_PBM) {
                rowRaster.setDataElements(0, 0, width, 1, planeData);
            }
            else {
                throw new AssertionError(String.format("Unsupported FORM type: %s", toChunkStr(header.formType)));
            }

            int dstY = (srcY - aoi.y) / sourceYSubsampling;
            // Handle non-converting raster as special case for performance
            if (cm.isCompatibleRaster(destRaster)) {
                // Rasters are compatible, just write to destination
                if (sourceXSubsampling == 1) {
                    destRaster.setRect(offset.x, dstY, sourceRow);
                }
                else {
                    for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) {
                        dataElements = sourceRow.getDataElements(srcX, 0, dataElements);
                        int dstX = /*offset.x +*/ srcX / sourceXSubsampling;
                        destRaster.setDataElements(dstX, dstY, dataElements);
                    }
                }
            }
            else {
                if (cm instanceof IndexColorModel) {
                    IndexColorModel icm = (IndexColorModel) cm;

                    for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) {
                        dataElements = sourceRow.getDataElements(srcX, 0, dataElements);
                        int rgb = icm.getRGB(dataElements);
                        outDataElements = destination.getColorModel().getDataElements(rgb, outDataElements);
                        int dstX = srcX / sourceXSubsampling;
                        destRaster.setDataElements(dstX, dstY, outDataElements);
                    }
                }
                else {
                    // TODO: This branch is never tested, and is probably "dead"
                    // ColorConvertOp
                    if (converter == null) {
                        converter = new ColorConvertOp(cm.getColorSpace(), destination.getColorModel().getColorSpace(), null);
                    }
                    converter.filter(
                            rowRaster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, null),
                            destRaster.createWritableChild(offset.x, offset.y + srcY - aoi.y, aoi.width, 1, 0, 0, null)
                    );
                }
            }

            processImageProgress(srcY * 100f / width);
            if (abortRequested()) {
                processReadAborted();
                break;
            }
        }
    }

    private void readChunky(final ImageReadParam param, final BufferedImage destination, final ImageInputStream input) throws IOException {
        final int width = header.width();
        final int height = header.height();

        final Rectangle aoi = getSourceRegion(param, width, height);
        final Point offset = param == null ? new Point(0, 0) : param.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 (param != null) {
            sourceXSubsampling = param.getSourceXSubsampling();
            sourceYSubsampling = param.getSourceYSubsampling();

            sourceBands = param.getSourceBands();
            destinationBands = param.getDestinationBands();
        }

        // Ensure band settings from param are compatible with images
        checkReadParamBandSettings(param, 4, destination.getSampleModel().getNumBands());

        WritableRaster destRaster = destination.getRaster();
        if (destinationBands != null || offset.x != 0 || offset.y != 0) {
            destRaster = destRaster.createWritableChild(0, 0, destRaster.getWidth(), destRaster.getHeight(), offset.x, offset.y, destinationBands);
        }

        ImageTypeSpecifier rawType = getRawImageType(0);
        WritableRaster rowRaster = rawType.createBufferedImage(width, 1).getRaster();
        Raster sourceRow = rowRaster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, sourceBands);

        int planeWidth = width * 4;

        final byte[] data = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
        Object dataElements = null;

        for (int srcY = 0; srcY < height; srcY++) {
            readPlaneData(data, 0, planeWidth, input);

            if (srcY >= aoi.y && (srcY - aoi.y) % sourceYSubsampling == 0) {
                int dstY = (srcY - aoi.y) / sourceYSubsampling;
                if (sourceXSubsampling == 1) {
                    destRaster.setRect(0, dstY, sourceRow);
                }
                else {
                    for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) {
                        dataElements = sourceRow.getDataElements(srcX, 0, dataElements);
                        int dstX = srcX / sourceXSubsampling;
                        destRaster.setDataElements(dstX, dstY, dataElements);
                    }
                }
            }

            processImageProgress(srcY * 100f / 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
    // bit of the red value for each pixel, and the last holds the most
    // significant bit of the blue value.
    private void readInterleaved(final ImageReadParam param, final BufferedImage destination, final ImageInputStream input) throws IOException {
        final int width = header.width();
        final int height = header.height();

        final Rectangle aoi = getSourceRegion(param, width, height);
        final Point offset = param == null ? new Point(0, 0) : param.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 (param != null) {
            sourceXSubsampling = param.getSourceXSubsampling();
            sourceYSubsampling = param.getSourceYSubsampling();

            sourceBands = param.getSourceBands();
            destinationBands = param.getDestinationBands();
        }

        // Ensure band settings from param are compatible with images
        checkReadParamBandSettings(param, header.bitplanes() / 8, destination.getSampleModel().getNumBands());

        // 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];

        WritableRaster destRaster = destination.getRaster();
        if (destinationBands != null || offset.x != 0 || offset.y != 0) {
            destRaster = destRaster.createWritableChild(0, 0, destRaster.getWidth(), destRaster.getHeight(), offset.x, offset.y, destinationBands);
        }

        WritableRaster rowRaster = destination.getRaster().createCompatibleWritableRaster(8 * planeWidth, 1);
        Raster sourceRow = rowRaster.createChild(aoi.x, 0, aoi.width, 1, 0, 0, sourceBands);

        final byte[] data = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
        final int channels = (header.bitplanes() + 7) / 8;
        final int planesPerChannel = 8;
        Object dataElements = null;

        for (int srcY = 0; srcY < height; srcY++) {
            for (int c = 0; c < channels; c++) {
                for (int p = 0; p < planesPerChannel; p++) {
                    readPlaneData(planeData, p * planeWidth, planeWidth, input);
                }

                // Skip rows outside AOI
                if (srcY >= (aoi.y + aoi.height)) {
                    return;
                }
                else if (srcY < aoi.y || (srcY - aoi.y) % sourceYSubsampling != 0) {
                    continue;
                }

                if (header.formType == IFF.TYPE_ILBM) {
                    // NOTE: Using (channels - c - 1) instead of just c,
                    // effectively reverses the channel order from RGBA to ABGR
                    int off = (channels - c - 1);

                    int pixelPos = 0;
                    for (int planePos = 0; planePos < planeWidth; planePos++) {
                        IFFUtil.bitRotateCW(planeData, planePos, planeWidth, data, off + pixelPos * channels, channels);
                        pixelPos += 8;
                    }
                }
                else if (header.formType == IFF.TYPE_PBM) {
                    System.arraycopy(planeData, 0, data, srcY * 8 * planeWidth, planeWidth);
                }
                else {
                    throw new AssertionError(String.format("Unsupported FORM type: %s", toChunkStr(header.formType)));
                }
            }

            if (srcY >= aoi.y && (srcY - aoi.y) % sourceYSubsampling == 0) {
                int dstY = (srcY - aoi.y) / sourceYSubsampling;
                // TODO: Avoid createChild if no region?
                if (sourceXSubsampling == 1) {
                    destRaster.setRect(0, dstY, sourceRow);
                }
                else {
                    for (int srcX = 0; srcX < sourceRow.getWidth(); srcX += sourceXSubsampling) {
                        dataElements = sourceRow.getDataElements(srcX, 0, dataElements);
                        int dstX = srcX / sourceXSubsampling;
                        destRaster.setDataElements(dstX, dstY, dataElements);
                    }
                }
            }

            processImageProgress(srcY * 100f / width);
            if (abortRequested()) {
                processReadAborted();
                break;
            }
        }
    }

    private void readPlaneData(final byte[] destination, final int offset, final int planeWidth, final ImageInputStream input)
            throws IOException {
        switch (header.compressionType()) {
            case BMHDChunk.COMPRESSION_NONE:
                input.readFully(destination, offset, planeWidth);

                // Uncompressed rows must have an even number of bytes
                if ((header.bitplanes() * planeWidth) % 2 != 0) {
                    input.readByte();
                }

                break;

            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 planeWidth,
                // to make sure we don't decode anything we don't have to (shouldn't).
                if (byteRunStream == null) {
                    byteRunStream = new DataInputStream(
                            new DecoderStream(
                                    IIOUtil.createStreamAdapter(input, header.bodyLength()),
                                    new PackBitsDecoder(header.sampleSize(), true),
                                    planeWidth * (header.sampleSize() > 1 ? 1 : header.bitplanes())
                            )
                    );
                }

                byteRunStream.readFully(destination, offset, planeWidth);
                break;

            case 4: // Compression type 4 means different things for different FORM types... :-P
                if (header.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(input, header.bodyLength()),
                                        new RGB8RLEDecoder(), 1024
                                )
                        );
                    }

                    byteRunStream.readFully(destination, offset, planeWidth);
                    break;
                }

            default:
                throw new IIOException(String.format("Unknown compression type: %d", header.compressionType()));
        }
    }

    private void multiPaletteToRGB(final int row, final byte[] indexed, final IndexColorModel colorModel, final byte[] dest, @SuppressWarnings("SameParameterValue") final int destOffset) {
        final int width = header.width();

        ColorModel palette = header.colorMapForRow(colorModel, row);

        for (int x = 0; x < width; x++) {
            int pixel = indexed[x] & 0xff;

            int rgb = palette.getRGB(pixel);

            int offset = (x * 3) + destOffset;
            dest[2 + offset] = (byte) ((rgb >> 16) & 0xff);
            dest[1 + offset] = (byte) ((rgb >>  8) & 0xff);
            dest[    offset] = (byte) ( rgb        & 0xff);
        }
    }

    private void hamToRGB(final byte[] indexed, final IndexColorModel colorModel, final byte[] dest, @SuppressWarnings("SameParameterValue") final int destOffset) {
        final int bits = header.bitplanes();
        final int width = header.width();

        //  Initialize to the "border color" (index 0)
        int lastRed = colorModel.getRed(0);
        int lastGreen = colorModel.getGreen(0);
        int lastBlue = colorModel.getBlue(0);

        for (int x = 0; x < width; x++) {
            int pixel = indexed[x] & 0xff;

            int paletteIndex = bits == 6 ? pixel & 0x0f : pixel & 0x3f;
            int indexShift = bits == 6 ? 4 : 2;
            int colorMask = bits == 6 ? 0x0f : 0x03;

            // Get Hold and Modify bits
            switch ((pixel >> (8 - indexShift)) & 0x03) {
                case 0x00:// HOLD
                    lastRed = colorModel.getRed(paletteIndex);
                    lastGreen = colorModel.getGreen(paletteIndex);
                    lastBlue = colorModel.getBlue(paletteIndex);
                    break;
                case 0x01:// MODIFY BLUE
                    lastBlue = (lastBlue & colorMask) | (paletteIndex << indexShift);
                    break;
                case 0x02:// MODIFY RED
                    lastRed = (lastRed & colorMask) | (paletteIndex << indexShift);
                    break;
                case 0x03:// MODIFY GREEN
                    lastGreen = (lastGreen & colorMask) | (paletteIndex << indexShift);
                    break;
            }

            int offset = (x * 3) + destOffset;
            dest[2 + offset] = (byte) lastRed;
            dest[1 + offset] = (byte) lastGreen;
            dest[    offset] = (byte) lastBlue;
        }
    }

    public static void main(String[] args) {
        ImageReader reader = new IFFImageReader(new IFFImageReaderSpi());

        boolean scale = false;
        for (String arg : args) {
            if (arg.startsWith("-")) {
                scale = true;
                continue;
            }

            File file = new File(arg);
            if (!file.isFile()) {
                continue;
            }

            try (ImageInputStream input = ImageIO.createImageInputStream(file)) {
                boolean canRead = reader.getOriginatingProvider().canDecodeInput(input);

                if (canRead) {
                    reader.setInput(input);
                    ImageReadParam param = reader.getDefaultReadParam();
    //            param.setSourceRegion(new Rectangle(0, 0, 160, 200));
    //            param.setSourceRegion(new Rectangle(160, 200, 160, 200));
    //            param.setSourceRegion(new Rectangle(80, 100, 160, 200));
    //            param.setDestinationOffset(new Point(80, 100));
    //            param.setSourceSubsampling(3, 3, 0, 0);
    //            param.setSourceBands(new int[]{0, 1, 2});
    //            param.setDestinationBands(new int[]{1, 0, 2});
                    BufferedImage image = reader.read(0, param);
                    System.out.println("image = " + image);

                    if (scale) {
                        image = new ResampleOp(image.getWidth() / 2, image.getHeight(), ResampleOp.FILTER_LANCZOS).filter(image, null);
    //                image = ImageUtil.createResampled(image, image.getWidth(), image.getHeight() * 2, Image.SCALE_FAST);
                    }

                    showIt(image, arg);
                }
                else {
                    System.err.println("Foo!");
                }
            }
            catch (IOException e) {
                System.err.println("Error reading file: " + file);
                e.printStackTrace();
            }
        }
    }
}