/*
 * Copyright (c) 2014, 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.psd;

import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.color.ColorSpaces;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;

import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
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.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Stack;

import static com.twelvemonkeys.imageio.plugins.psd.PSDUtil.createDecompressorStream;

/**
 * ImageReader for Adobe Photoshop Document (PSD) format.
 *
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @author last modified by $Author: haraldk$
 * @version $Id: PSDImageReader.java,v 1.0 Apr 29, 2008 4:45:52 PM haraldk Exp$
 * @see <a href="http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/">Adobe Photoshop File Formats Specification</a>
 * @see <a href="http://www.fileformat.info/format/psd/egff.htm">Adobe Photoshop File Format Summary</a>
 */
// TODO: Rethink metadata impl.
//  * We should probably move most of the current impl to stream metadata as it belongs to the document, not the individual layers
//  * Make each layer's metadata contain correct data, name, sub-image type, position etc.
//  * Retain some information in the merged image/layers?
//  * Completely skip the non-pixel layers in the reader (no longer return null, that's just ugly)
// TODO: Figure out of we should assume Adobe RGB (1998) color model, if no embedded profile?
// TODO: Support for PSDVersionInfo hasRealMergedData=false (no real composite data, layers will be in index 0)
// TODO: Consider Romain Guy's Java 2D implementation of PS filters for the blending modes in layers
// http://www.curious-creature.org/2006/09/20/new-blendings-modes-for-java2d/
// See http://www.codeproject.com/KB/graphics/PSDParser.aspx
// See http://www.adobeforums.com/webx?14@@.3bc381dc/0  
// Done: Allow reading the extra alpha channels (index after composite data)
// Done: Implement ImageIO meta data interface
public final class PSDImageReader extends ImageReaderBase {

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

    private PSDHeader header;
    private ICC_ColorSpace colorSpace;
    private PSDMetadata metadata;

    PSDImageReader(final ImageReaderSpi originatingProvider) {
        super(originatingProvider);
    }

    protected void resetMembers() {
        header = null;
        metadata = null;
        colorSpace = null;
    }

    public int getWidth(final int imageIndex) throws IOException {
        checkBounds(imageIndex);
        readHeader();

        if (imageIndex > 0) {
            return getLayerWidth(imageIndex - 1);
        }

        return header.width;
    }

    public int getHeight(final int imageIndex) throws IOException {
        checkBounds(imageIndex);
        readHeader();

        if (imageIndex > 0) {
            return getLayerHeight(imageIndex - 1);
        }

        return header.height;
    }

    private int getLayerWidth(int layerIndex) throws IOException {
        readLayerAndMaskInfo(true);

        PSDLayerInfo layerInfo = metadata.layerInfo.get(layerIndex);

        return layerInfo.right - layerInfo.left;
    }

    private int getLayerHeight(int layerIndex) throws IOException {
        readLayerAndMaskInfo(true);

        PSDLayerInfo layerInfo = metadata.layerInfo.get(layerIndex);

        return layerInfo.bottom - layerInfo.top;
    }

    @Override
    public ImageTypeSpecifier getRawImageType(final int imageIndex) throws IOException {
        return getRawImageTypeInternal(imageIndex);
    }

    private ImageTypeSpecifier getRawImageTypeInternal(final int imageIndex) throws IOException {
        checkBounds(imageIndex);

        // Image index above 0, means a layer
        if (imageIndex > 0) {
            readLayerAndMaskInfo(true);

            return getRawImageTypeForLayer(imageIndex - 1);
        }

        // Otherwise, get the type specifier for the composite layer
        return getRawImageTypeForCompositeLayer();
    }

    private ImageTypeSpecifier getRawImageTypeForCompositeLayer() throws IOException {
        ColorSpace cs;

        switch (header.mode) {
            case PSD.COLOR_MODE_BITMAP:
                if (header.channels == 1 && header.bits == 1) {
                    return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_BINARY);
                }

                throw new IIOException(String.format("Unsupported channel count/bit depth for Monochrome PSD: %d channels/%d bits", header.channels, header.bits));

            case PSD.COLOR_MODE_INDEXED:
                if (header.channels == 1 && header.bits == 8) {
                    return ImageTypeSpecifiers.createFromIndexColorModel(metadata.colorData.getIndexColorModel());
                }

                throw new IIOException(String.format("Unsupported channel count/bit depth for Indexed Color PSD: %d channels/%d bits", header.channels, header.bits));

            case PSD.COLOR_MODE_DUOTONE:
                // NOTE: Duotone (whatever that is) should be treated as gray scale
                // Fall-through
            case PSD.COLOR_MODE_GRAYSCALE:
                cs = getEmbeddedColorSpace();
                if (cs == null) {
                    cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
                }

                if (header.channels >= 1) {
                    switch (header.bits) {
                        case 8:
                            return metadata.hasAlpha() && header.channels > 1
                                   ? ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1}, new int[] {0, 0}, DataBuffer.TYPE_BYTE, true, false)
                                   : ImageTypeSpecifiers.createBanded(cs, new int[] {0}, new int[] {0}, DataBuffer.TYPE_BYTE, false, false);
                        case 16:
                            return metadata.hasAlpha() && header.channels > 1
                                   ? ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1}, new int[] {0, 0}, DataBuffer.TYPE_USHORT, true, false)
                                   : ImageTypeSpecifiers.createBanded(cs, new int[] {0}, new int[] {0}, DataBuffer.TYPE_USHORT, false, false);
                        case 32:
                            return metadata.hasAlpha() && header.channels > 1
                                   ? ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1}, new int[] {0, 0}, DataBuffer.TYPE_INT, true, false)
                                   : ImageTypeSpecifiers.createBanded(cs, new int[] {0}, new int[] {0}, DataBuffer.TYPE_INT, false, false);
                    }
                }

                throw new IIOException(String.format("Unsupported channel count/bit depth for Gray Scale PSD: %d channels/%d bits", header.channels, header.bits));

            case PSD.COLOR_MODE_RGB:
                cs = getEmbeddedColorSpace();
                if (cs == null) {
                    cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
                }

                if (header.channels >= 3) {
                    switch (header.bits) {
                        case 8:
                            return metadata.hasAlpha() && header.channels > 3
                                   ? ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_BYTE, true, false)
                                   : ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2}, new int[] {0, 0, 0}, DataBuffer.TYPE_BYTE, false, false);
                        case 16:
                            return metadata.hasAlpha() && header.channels > 3
                                   ? ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_USHORT, true, false)
                                   : ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2}, new int[] {0, 0, 0}, DataBuffer.TYPE_USHORT, false, false);
                        case 32:
                            return metadata.hasAlpha() && header.channels > 3
                                   ? ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_INT, true, false)
                                   : ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2}, new int[] {0, 0, 0}, DataBuffer.TYPE_INT, false, false);

                    }
                }

                throw new IIOException(String.format("Unsupported channel count/bit depth for RGB PSD: %d channels/%d bits", header.channels, header.bits));

            case PSD.COLOR_MODE_CMYK:
                cs = getEmbeddedColorSpace();
                if (cs == null) {
                    cs = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK);
                }

                if (header.channels >= 4) {
                    switch (header.bits) {
                        case 8:
                            return metadata.hasAlpha() && header.channels > 4
                                   ? ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, DataBuffer.TYPE_BYTE, true, false)
                                   : ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_BYTE, false, false);
                        case 16:
                            return metadata.hasAlpha() && header.channels > 4
                                   ? ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, DataBuffer.TYPE_USHORT, true, false)
                                   : ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_USHORT, false, false);
                        case 32:
                            return metadata.hasAlpha() && header.channels > 4
                                   ? ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, DataBuffer.TYPE_INT, true, false)
                                   : ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_INT, false, false);

                    }
                }

                throw new IIOException(String.format("Unsupported channel count/bit depth for CMYK PSD: %d channels/%d bits", header.channels, header.bits));

            case PSD.COLOR_MODE_MULTICHANNEL:
                // TODO: Implement
            case PSD.COLOR_MODE_LAB:
                // TODO: Implement
                // TODO: If there's a color profile embedded, it should be easy, otherwise we're out of luck...
                // TODO: See the LAB color handling in TIFF
            default:
                throw new IIOException(String.format("Unsupported PSD MODE: %s (%d channels/%d bits)", header.mode, header.channels, header.bits));
        }
    }

    public Iterator<ImageTypeSpecifier> getImageTypes(final int imageIndex) throws IOException {
        // TODO: Check out the custom ImageTypeIterator and ImageTypeProducer used in the Sun provided JPEGImageReader
        // Could use similar concept to create lazily-created ImageTypeSpecifiers (util candidate, based on FilterIterator?)

        // Get the raw type. Will fail for unsupported types
        ImageTypeSpecifier rawType = getRawImageTypeInternal(imageIndex);

        ColorSpace cs = rawType.getColorModel().getColorSpace();
        List<ImageTypeSpecifier> types = new ArrayList<>();

        switch (header.mode) {
            case PSD.COLOR_MODE_GRAYSCALE:
                if (rawType.getNumBands() == 1 && rawType.getBitsPerBand(0) == 8) {
                    types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY));
                }
                else if (rawType.getNumBands() >= 2 && rawType.getBitsPerBand(0) == 8) {
                    types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[] {1, 0}, DataBuffer.TYPE_BYTE, true, false));
                }
                else if (rawType.getNumBands() == 1 && rawType.getBitsPerBand(0) == 16) {
                    types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_USHORT_GRAY));
                }
                else if (rawType.getNumBands() >= 2 && rawType.getBitsPerBand(0) == 16) {
                    types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[] {1, 0}, DataBuffer.TYPE_USHORT, true, false));
                }
                break;
            case PSD.COLOR_MODE_RGB:
                // Prefer interleaved versions as they are much faster to display
                if (rawType.getNumBands() == 3 && rawType.getBitsPerBand(0) == 8) {
                    // TODO: Integer raster
                    // types.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.INT_RGB));
                    types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR));

                    if (!cs.isCS_sRGB()) {
                        // Basically BufferedImage.TYPE_3BYTE_BGR, with corrected ColorSpace. Possibly slow.
                        types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[] {2, 1, 0}, DataBuffer.TYPE_BYTE, false, false));
                    }
                }
                else if (rawType.getNumBands() >= 4 && rawType.getBitsPerBand(0) == 8) {
                    // TODO: Integer raster
                    // types.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.INT_ARGB));
                    types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR));

                    if (!cs.isCS_sRGB()) {
                        // Basically BufferedImage.TYPE_4BYTE_ABGR, with corrected ColorSpace. Possibly slow.
                        types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, true, false));
                    }
                }
                else if (rawType.getNumBands() == 3 && rawType.getBitsPerBand(0) == 16) {
                    types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[] {2, 1, 0}, DataBuffer.TYPE_USHORT, false, false));
                }
                else if (rawType.getNumBands() >= 4 && rawType.getBitsPerBand(0) == 16) {
                    types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_USHORT, true, false));
                }
                break;
            case PSD.COLOR_MODE_CMYK:
                // Prefer interleaved versions as they are much faster to display
                // TODO: We should convert these to their RGB equivalents while reading for the common-case,
                // as Java2D is extremely slow displaying custom images.
                // Converting to RGB is also correct behaviour, according to the docs.
                // Doing this, will require rewriting the image reading, as the raw image data is channelled, not interleaved :-/
                if (rawType.getNumBands() == 4 && rawType.getBitsPerBand(0) == 8) {
                    types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false));
                }
                else if (rawType.getNumBands() == 5 && rawType.getBitsPerBand(0) == 8) {
                    types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[] {4, 3, 2, 1, 0}, DataBuffer.TYPE_BYTE, true, false));
                }
                else if (rawType.getNumBands() == 4 && rawType.getBitsPerBand(0) == 16) {
                    types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_USHORT, false, false));
                }
                else if (rawType.getNumBands() == 5 && rawType.getBitsPerBand(0) == 16) {
                    types.add(ImageTypeSpecifiers.createInterleaved(cs, new int[] {4, 3, 2, 1, 0}, DataBuffer.TYPE_USHORT, true, false));
                }
                break;
            default:
                // Just stick to the raw type
        }

        // Finally, add the raw type
        types.add(rawType);

        return types.iterator();
    }

    private ColorSpace getEmbeddedColorSpace() throws IOException {
        readImageResources(true);

        if (colorSpace == null) {
            ICC_Profile profile = null;
            for (PSDImageResource resource : metadata.imageResources) {
                if (resource instanceof ICCProfile) {
                    profile = ((ICCProfile) resource).getProfile();
                    break;
                }
            }

            colorSpace = profile == null ? null : ColorSpaces.createColorSpace(profile);
        }

        return colorSpace;
    }

    public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException {
        checkBounds(imageIndex);

        // TODO: What about the extra alpha channels possibly present? Read as gray scale as extra images?

        // Layer hacks... For now, any index above 0 is considered to be a layer...
        // TODO: Support layer in index 0, if "has real merged data" flag is false?
        // TODO: Param support in layer code (more duping/cleanup..)
        if (imageIndex > 0) {
            return readLayerData(imageIndex - 1, param);
        }

        BufferedImage image = getDestination(param, getImageTypes(imageIndex), header.width, header.height);
        ImageTypeSpecifier rawType = getRawImageType(imageIndex);
        checkReadParamBandSettings(param, rawType.getNumBands(), image.getSampleModel().getNumBands());

        final Rectangle source = new Rectangle();
        final Rectangle dest = new Rectangle();
        computeRegions(param, header.width, header.height, image, source, dest);

        final int xSub;
        final int ySub;

        if (param == null) {
            xSub = ySub = 1;
        }
        else {
            xSub = param.getSourceXSubsampling();
            ySub = param.getSourceYSubsampling();
        }

        imageInput.seek(metadata.imageDataStart);
        int compression = imageInput.readShort();
        metadata.compression = compression;

        int[][] byteCounts = null;
        switch (compression) {
            case PSD.COMPRESSION_ZIP:
            case PSD.COMPRESSION_ZIP_PREDICTION:
            case PSD.COMPRESSION_NONE:
                break;
            case PSD.COMPRESSION_RLE:
                // NOTE: Byte counts will allow us to easily skip rows before AOI
                byteCounts = new int[header.channels][header.height];
                for (int c = 0; c < header.channels; c++) {
                    for (int y = 0; y < header.height; y++) {
                        byteCounts[c][y] = header.largeFormat ? imageInput.readInt() : imageInput.readUnsignedShort();
                    }
                }
                break;
            default:
                throw new IIOException(
                        String.format(
                                "Unknown PSD compression: %d. Expected 0 (none), 1 (RLE), 2 (ZIP) or 3 (ZIP w/prediction).",
                                compression
                        )
                );
        }

        processImageStarted(imageIndex);

        // What we read here is the "composite layer" of the PSD file
        readImageData(image, rawType.getColorModel(), source, dest, xSub, ySub, byteCounts, compression);

        if (abortRequested()) {
            processReadAborted();
        }
        else {
            processImageComplete();
        }

        return image;
    }

    private long findLayerStartPos(int layerIndex) {
        long layersStart = metadata.layersStart;

        for (int i = 0; i < layerIndex; i++) {
            PSDLayerInfo layerInfo = metadata.layerInfo.get(i);

            for (PSDChannelInfo channelInfo : layerInfo.channelInfo) {
                layersStart += channelInfo.length;
            }
        }

        return layersStart;
    }

    private void readImageData(final BufferedImage destination,
                               final ColorModel pSourceCM, final Rectangle pSource, final Rectangle pDest,
                               final int pXSub, final int pYSub,
                               final int[][] byteCounts, final int compression) throws IOException {

        WritableRaster destRaster = destination.getRaster();
        ColorModel destCM = destination.getColorModel();

        int channels = pSourceCM.createCompatibleSampleModel(1, 1).getNumBands();
        ImageTypeSpecifier singleBandRowSpec = ImageTypeSpecifiers.createGrayscale(header.bits, pSourceCM.getTransferType());
        WritableRaster rowRaster = singleBandRowSpec.createBufferedImage(header.width, 1).getRaster();
        boolean banded = destRaster.getDataBuffer().getNumBanks() > 1;
        int interleavedBands = banded ? 1 : destRaster.getNumBands();

        for (int c = 0; c < channels; c++) {
            try (ImageInputStream stream = createDecompressorStream(imageInput, compression, header.width, header.bits, byteCounts != null ? byteCounts[c] : null, -1)) {
                int bandOffset = banded ? 0 : interleavedBands - 1 - c;

                switch (header.bits) {
                    case 1:
                        byte[] row1 = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
                        read1bitChannel(stream, c, destRaster.getDataBuffer(), row1, pSource, pDest, pXSub, pYSub, header.width, header.height);
                        break;
                    case 8:
                        byte[] row8 = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
                        read8bitChannel(stream, c, channels, destRaster.getDataBuffer(), c, interleavedBands, bandOffset, pSourceCM, row8, pSource, pDest, pXSub, pYSub, header.width, header.height);
                        break;
                    case 16:
                        short[] row16 = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
                        read16bitChannel(stream, c, channels, destRaster.getDataBuffer(), c, interleavedBands, bandOffset, pSourceCM, row16, pSource, pDest, pXSub, pYSub, header.width, header.height);
                        break;
                    case 32:
                        int[] row32 = ((DataBufferInt) rowRaster.getDataBuffer()).getData();
                        read32bitChannel(stream, c, channels, destRaster.getDataBuffer(), c, interleavedBands, bandOffset, pSourceCM, row32, pSource, pDest, pXSub, pYSub, header.width, header.height);
                        break;
                    default:
                        throw new IIOException(String.format("Unsupported PSD bit depth: %s", header.bits));
                }

                if (abortRequested()) {
                    break;
                }
            }
        }

        if (header.bits == 8) {
            // Compose out the background of the semi-transparent pixels, as PS somehow has the background composed in
            decomposeAlpha(destCM, destRaster.getDataBuffer(), pDest.width, pDest.height, destRaster.getNumBands());
        }

        // NOTE: ColorSpace uses Object.equals(), so we rely on using same instances!
        if (!pSourceCM.getColorSpace().equals(destCM.getColorSpace())) {
            convertToDestinationCS(pSourceCM, destCM, destRaster);
        }
    }

    private void convertToDestinationCS(final ColorModel sourceCM, ColorModel destinationCM, final WritableRaster raster) {
        long start = DEBUG ? System.currentTimeMillis() : 0;

        // Color conversion from embedded color space, to destination color space
        WritableRaster alphaMaskedRaster = destinationCM.hasAlpha()
                                           ? raster.createWritableChild(0, 0, raster.getWidth(), raster.getHeight(),
                raster.getMinX(), raster.getMinY(),
                createBandList(sourceCM.getColorSpace().getNumComponents()))
                                           : raster;

        new ColorConvertOp(sourceCM.getColorSpace(), destinationCM.getColorSpace(), null)
                .filter(alphaMaskedRaster, alphaMaskedRaster);

        if (DEBUG) {
            System.out.println("Color conversion " + (System.currentTimeMillis() - start) + "ms");
        }
    }

    private int[] createBandList(final int numBands) {
        int[] bands = new int[numBands];

        for (int i = 0; i < numBands; i++) {
            bands[i] = i;
        }

        return bands;
    }

    private void processImageProgressForChannel(int channel, int channelCount, int y, int height) {
        processImageProgress(100f * channel / channelCount + 100f * y / (height * channelCount));
    }

    private void read32bitChannel(final ImageInputStream stream,
                                  final int channel, final int channelCount,
                                  final DataBuffer data,
                                  final int band, final int bandCount, final int bandOffset,
                                  final ColorModel sourceColorModel,
                                  final int[] rowData,
                                  final Rectangle sourceRect, final Rectangle destRect,
                                  final int xSub, final int ySub,
                                  final int channelWidth, final int channelHeight) throws IOException {

        boolean isCMYK = sourceColorModel.getColorSpace().getType() == ColorSpace.TYPE_CMYK;
        int colorComponents = sourceColorModel.getColorSpace().getNumComponents();
        final boolean invert = isCMYK && band < colorComponents;
        final boolean banded = data.getNumBanks() > 1;

        for (int y = 0; y < channelHeight; y++) {
            // TODO: Sometimes need to read the line y == source.y + source.height...
            // Read entire line, if within source region and sampling
            if (y >= sourceRect.y && y < sourceRect.y + sourceRect.height && y % ySub == 0) {
                stream.readFully(rowData, 0, channelWidth);

                // TODO: Destination offset...??
                // Copy line sub sampled into real data
                int offset = (y - sourceRect.y) / ySub * destRect.width * bandCount + bandOffset;
                for (int x = 0; x < destRect.width; x++) {
                    int value = rowData[sourceRect.x + x * xSub];

                    // CMYK values are stored inverted, but alpha is not
                    if (invert) {
                        value = 0xffffffff - value;
                    }

                    data.setElem(banded ? band : 0, offset + x * bandCount, value);
                }
            }
            else {
                stream.skipBytes(4 * channelWidth);
            }

            if (abortRequested()) {
                break;
            }

            processImageProgressForChannel(channel, channelCount, y, channelHeight);
        }
    }

    private void read16bitChannel(final ImageInputStream stream,
                                  final int channel, final int channelCount,
                                  final DataBuffer data,
                                  final int band, final int bandCount, final int bandOffset,
                                  final ColorModel sourceColorModel,
                                  final short[] rowData,
                                  final Rectangle sourceRect, final Rectangle destRect,
                                  final int xSub, final int ySub,
                                  final int channelWidth, final int channelHeight) throws IOException {

        boolean isCMYK = sourceColorModel.getColorSpace().getType() == ColorSpace.TYPE_CMYK;
        int colorComponents = sourceColorModel.getColorSpace().getNumComponents();
        final boolean invert = isCMYK && band < colorComponents;
        final boolean banded = data.getNumBanks() > 1;

        for (int y = 0; y < channelHeight; y++) {
            // TODO: Sometimes need to read the line y == source.y + source.height...
            // Read entire line, if within source region and sampling
            if (y >= sourceRect.y && y < sourceRect.y + sourceRect.height && y % ySub == 0) {
                stream.readFully(rowData, 0, channelWidth);

                // TODO: Destination offset...??
                // Copy line sub sampled into real data
                int offset = (y - sourceRect.y) / ySub * destRect.width * bandCount + bandOffset;
                for (int x = 0; x < destRect.width; x++) {
                    short value = rowData[sourceRect.x + x * xSub];

                    // CMYK values are stored inverted, but alpha is not
                    if (invert) {
                        value = (short) (0xffff - value & 0xffff);
                    }

                    data.setElem(banded ? band : 0, offset + x * bandCount, value);
                }
            }
            else {
                stream.skipBytes(2 * channelWidth);
            }

            if (abortRequested()) {
                break;
            }

            processImageProgressForChannel(channel, channelCount, y, channelHeight);
        }
    }

    private void read8bitChannel(final ImageInputStream stream,
                                 final int channel, final int channelCount,
                                 final DataBuffer data,
                                 final int band, final int bandCount, final int bandOffset,
                                 final ColorModel sourceColorModel,
                                 final byte[] rowData,
                                 final Rectangle sourceRect, final Rectangle destRect,
                                 final int xSub, final int ySub,
                                 final int channelWidth, final int channelHeight) throws IOException {

        boolean isCMYK = sourceColorModel.getColorSpace().getType() == ColorSpace.TYPE_CMYK;
        int colorComponents = sourceColorModel.getColorSpace().getNumComponents();
        final boolean invert = isCMYK && band < colorComponents;
        final boolean banded = data.getNumBanks() > 1;

        for (int y = 0; y < channelHeight; y++) {
            // TODO: Sometimes need to read the line y == source.y + source.height...
            // Read entire line, if within source region and sampling
            if (y >= sourceRect.y && y < sourceRect.y + sourceRect.height && y % ySub == 0) {
                stream.readFully(rowData, 0, channelWidth);

                // TODO: Destination offset...??
                // Copy line sub sampled into real data
                int offset = (y - sourceRect.y) / ySub * destRect.width * bandCount + bandOffset;
                for (int x = 0; x < destRect.width; x++) {
                    byte value = rowData[sourceRect.x + x * xSub];

                    // CMYK values are stored inverted, but alpha is not
                    if (invert) {
                        value = (byte) (0xff - value & 0xff);
                    }

                    data.setElem(banded ? band : 0, offset + x * bandCount, value);
                }
            }
            else {
                stream.skipBytes(channelWidth);
            }

            if (abortRequested()) {
                break;
            }

            processImageProgressForChannel(channel, channelCount, y, channelHeight);
        }
    }

    private void read1bitChannel(final ImageInputStream stream,
                                 final int channel,
                                 final DataBuffer data,
                                 final byte[] rowData,
                                 final Rectangle sourceRect, final Rectangle destRect,
                                 final int xSub, final int ySub,
                                 final int channelWidth, final int channelHeight) throws IOException {
        // NOTE: 1 bit channels only occurs once
        if (channel > 0) {
            throw new IIOException("Multiple channels not supported for 1 bit data");
        }

        final int destWidth = (destRect.width + 7) / 8;
        final boolean banded = data.getNumBanks() > 1;

        for (int y = 0; y < channelHeight; y++) {
            // TODO: Sometimes need to read the line y == source.y + source.height...
            // Read entire line, if within source region and sampling
            if (y >= sourceRect.y && y < sourceRect.y + sourceRect.height && y % ySub == 0) {
                stream.readFully(rowData, 0, rowData.length);

                // TODO: Destination offset...??
                int offset = (y - sourceRect.y) / ySub * destWidth;
                if (xSub == 1 && sourceRect.x % 8 == 0) {
                    // Fast normal case, no sub sampling
                    for (int i = 0; i < destWidth; i++) {
                        byte value = rowData[sourceRect.x / 8 + i * xSub];
                        // NOTE: Invert bits to match Java's default monochrome
                        data.setElem(banded ? channel : 0, offset + i, (byte) (~value & 0xff));
                    }
                }
                else {
                    // Copy line sub sampled into real data
                    final int maxX = sourceRect.x + sourceRect.width;
                    int x = sourceRect.x;
                    for (int i = 0; i < destWidth; i++) {
                        byte result = 0;

                        for (int j = 0; j < 8 && x < maxX; j++) {
                            int bytePos = x / 8;

                            int sourceBitOff = 7 - (x % 8);
                            int mask = 1 << sourceBitOff;
                            int destBitOff = 7 - j;

                            // Shift bit into place
                            result |= ((rowData[bytePos] & mask) >> sourceBitOff) << destBitOff;

                            x += xSub;
                        }

                        // NOTE: Invert bits to match Java's default monochrome
                        data.setElem(banded ? channel : 0, offset + i, (byte) (~result & 0xff));
                    }
                }
            }
            else {
                stream.skipBytes((channelWidth + 7) / 8);
            }

            if (abortRequested()) {
                break;
            }

            processImageProgressForChannel(channel, 1, y, channelHeight);
        }
    }

    private void decomposeAlpha(final ColorModel pModel, final DataBuffer pBuffer,
                                final int pWidth, final int pHeight, final int pChannels) {
        // NOTE: It seems that the document background always white..?!
        // TODO: What about CMYK + alpha?
        if (pModel.hasAlpha() && pModel.getColorSpace().getType() == ColorSpace.TYPE_RGB) {

            // TODO: Probably faster to do this in line..
            if (pBuffer.getNumBanks() > 1) {
                for (int y = 0; y < pHeight; y++) {
                    for (int x = 0; x < pWidth; x++) {
                        int offset = (x + y * pWidth);
                        // ARGB format
                        int alpha = pBuffer.getElem(pChannels - 1, offset) & 0xff;

                        if (alpha != 0) {
                            double normalizedAlpha = alpha / 255.0;

                            for (int i = 0; i < pChannels - 1; i++) {
                                pBuffer.setElem(i, offset, decompose(pBuffer.getElem(i, offset) & 0xff, normalizedAlpha));
                            }
                        }
                        else {
                            for (int i = 0; i < pChannels - 1; i++) {
                                pBuffer.setElem(i, offset, 0);
                            }
                        }
                    }
                }
            }
            else {
                for (int y = 0; y < pHeight; y++) {
                    for (int x = 0; x < pWidth; x++) {
                        int offset = (x + y * pWidth) * pChannels;
                        // ABGR format
                        int alpha = pBuffer.getElem(offset) & 0xff;

                        if (alpha != 0) {
                            double normalizedAlpha = alpha / 255.0;

                            for (int i = 1; i < pChannels; i++) {
                                pBuffer.setElem(offset + i, decompose(pBuffer.getElem(offset + i) & 0xff, normalizedAlpha));
                            }
                        }
                        else {
                            for (int i = 1; i < pChannels; i++) {
                                pBuffer.setElem(offset + i, 0);
                            }
                        }
                    }
                }
            }
        }
    }

    private static byte decompose(final int pColor, final double pAlpha) {
        // Adapted from Computer Graphics: Principles and Practice (Foley et al.), p. 837
        double color = pColor / 255.0;
        return (byte) ((color / pAlpha - ((1 - pAlpha) / pAlpha)) * 255);
    }

    private void readHeader() throws IOException {
        assertInput();

        if (header == null) {
            header = PSDHeader.read(imageInput);

            if (!header.hasValidDimensions()) {
                processWarningOccurred(String.format("Dimensions exceed maximum allowed for %s: %dx%d (max %dx%d)",
                        header.largeFormat ? "PSB" : "PSD",
                        header.width, header.height, header.getMaxSize(), header.getMaxSize()));
            }

            metadata = new PSDMetadata();
            metadata.header = header;

            // Contains the required data to define the color mode.
            //
            // For indexed color images, the count will be equal to 768, and the mode data
            // will contain the color table for the image, in non-interleaved order.
            //
            // For duotone images, the mode data will contain the duotone specification,
            // the format of which is not documented.  Non-Photoshop readers can treat
            // the duotone image as a grayscale image, and keep the duotone specification
            // around as a black box for use when saving the file.
            if (header.mode == PSD.COLOR_MODE_INDEXED) {
                metadata.colorData = new PSDColorData(imageInput);
            }
            else {
                // TODO: We need to store the duotone spec if we decide to create a writer...
                // Skip color mode data for other modes
                long length = imageInput.readUnsignedInt();
                imageInput.skipBytes(length);
            }

            metadata.imageResourcesStart = imageInput.getStreamPosition();

            // Don't need the header again
            imageInput.flushBefore(imageInput.getStreamPosition());

            if (DEBUG) {
                System.out.println("header: " + header);
            }
        }
    }

    // TODO: Flags or list of interesting resources to parse
    // TODO: Obey ignoreMetadata
    private void readImageResources(final boolean pParseData) throws IOException {
        readHeader();

        if (pParseData && metadata.imageResources == null || metadata.layerAndMaskInfoStart == 0) {
            imageInput.seek(metadata.imageResourcesStart);

            long imageResourcesLength = imageInput.readUnsignedInt();

            if (pParseData && metadata.imageResources == null) {
                if (imageResourcesLength == 0) {
                    metadata.imageResources = Collections.emptyList();
                }
                else {
                    metadata.imageResources = new ArrayList<>();

                    long expectedEnd = imageInput.getStreamPosition() + imageResourcesLength;

                    while (imageInput.getStreamPosition() < expectedEnd) {
                        PSDImageResource resource = PSDImageResource.read(imageInput);
                        metadata.imageResources.add(resource);
                    }

                    if (DEBUG) {
                        System.out.println("imageResources: " + metadata.imageResources);
                    }

                    if (imageInput.getStreamPosition() != expectedEnd) {
                        throw new IIOException("Corrupt PSD document"); // ..or maybe just a bug in the reader.. ;-)
                    }
                }
            }

            // TODO: We should now be able to flush input
//                imageInput.flushBefore(metadata.imageResourcesStart + imageResourcesLength + 4);

            metadata.layerAndMaskInfoStart = metadata.imageResourcesStart + imageResourcesLength + 4; // + 4 for the length field itself
        }
    }

    // TODO: Flags or list of interesting resources to parse
    // TODO: Obey ignoreMetadata
    private void readLayerAndMaskInfo(final boolean parseData) throws IOException {
        readImageResources(false);

        if (parseData && (metadata.layerInfo == null || metadata.globalLayerMask == null) || metadata.imageDataStart == 0) {
            imageInput.seek(metadata.layerAndMaskInfoStart);

            long layerAndMaskInfoLength = readLength(imageInput);

            // NOTE: The spec says that if this section is empty, the length should be 0.
            // Yet I have a PSB file that has size 12, and both contained lengths set to 0 (which
            // is also not as per spec, as layer count should be included if there's a layer info
            // block, so minimum size should be either 0 or 14 (or 16 if multiple of 4 for PSB))...

            if (layerAndMaskInfoLength > 0) {
                long pos = imageInput.getStreamPosition();

                long layerInfoLength = readLength(imageInput);

                if (layerInfoLength > 0) {
                    // "Layer count. If it is a negative number, its absolute value is the number of
                    // layers and the first alpha channel contains the transparency data for the
                    // merged result."
                    int layerCount = imageInput.readShort();
                    metadata.layerCount = layerCount;

                    if (metadata.layerInfo == null) {
                        metadata.layerInfo = readLayerInfo(Math.abs(layerCount));
                        metadata.layersStart = imageInput.getStreamPosition();
                    }

                    long read = imageInput.getStreamPosition() - pos;
                    long diff = layerInfoLength - (read - (header.largeFormat ? 8 : 4)); // - 8 or 4 for the layerInfoLength field itself

                    imageInput.skipBytes(diff);
                }

                // Global LayerMaskInfo (18 bytes or more..?)
                // 4 (length), 2 (colorSpace), 8 (4 * 2 byte color components), 2 (opacity %), 1 (kind), variable (pad)
                long globalLayerMaskInfoLength = imageInput.readUnsignedInt(); // NOTE: Not long for PSB!

                if (globalLayerMaskInfoLength > 0) {
                    if (parseData && metadata.globalLayerMask == null) {
                        metadata.globalLayerMask = new PSDGlobalLayerMask(imageInput, globalLayerMaskInfoLength);
                    }
                    // TODO: Else skip?
                }
                else {
                    metadata.globalLayerMask = PSDGlobalLayerMask.NULL_MASK;
                }

                if (metadata.layerInfo == null) {
                    while (imageInput.getStreamPosition() + 12 < metadata.layerAndMaskInfoStart + layerAndMaskInfoLength) {
                        int resSig = imageInput.readInt();
                        if (resSig != PSD.RESOURCE_TYPE && resSig != PSD.RESOURCE_TYPE_LONG) {
                            processWarningOccurred(String.format("Bad resource alignment, expected: '8BIM' was '%s'", PSDUtil.intToStr(resSig)));
                            break;
                        }

                        int resId = imageInput.readInt();
                        long resLength = readLength(imageInput, resId); // In this section, resource lengths *vary* based on the resource...
                        // Calculate next offset, for some reason lengths are padded to 32 bit...
                        long nextOffset = imageInput.getStreamPosition() + 4 * ((resLength + 3) / 4);

                        if (DEBUG) {
                            System.out.println("resId: " + PSDUtil.intToStr(resId));
                            System.out.println("length = " + resLength);
                            System.out.printf("nextOffset = %d%n", nextOffset);
                        }

                        switch (resId) {
                            case PSD.Layr:
                            case PSD.Lr16:
                            case PSD.Lr32:
                                short layerCount = imageInput.readShort();

                                metadata.layerCount = layerCount;
                                metadata.layerInfo = readLayerInfo(Math.abs(layerCount));
                                metadata.layersStart = imageInput.getStreamPosition();
                                break;
                            default:
                        }

                        imageInput.seek(nextOffset);
                    }
                }

                if (parseData && metadata.layerInfo == null) {
                    // We have parsed but didn't find any layers
                    metadata.layerInfo = Collections.emptyList();
                }

                // TODO: We should now be able to flush input
//                    imageInput.seek(metadata.layerAndMaskInfoStart + layerAndMaskInfoLength + (header.largeFormat ? 8 : 4));
//                    imageInput.flushBefore(metadata.layerAndMaskInfoStart + layerAndMaskInfoLength + (header.largeFormat ? 8 : 4));

                if (parseData && DEBUG) {
                    System.out.println("layerInfo: " + metadata.layerInfo);
                    System.out.println("globalLayerMask: " + (metadata.globalLayerMask != PSDGlobalLayerMask.NULL_MASK ? metadata.globalLayerMask : null));
                }
            }

            metadata.imageDataStart = metadata.layerAndMaskInfoStart + layerAndMaskInfoLength + (header.largeFormat ? 8 : 4);
        }
    }

    private long readLength(final ImageInputStream stream) throws IOException {
        return header.largeFormat
               ? stream.readLong()
               : stream.readUnsignedInt();
    }

    private long readLength(final ImageInputStream stream, final int resId) throws IOException {
        // Only the following resources use long (64 bit) lengths:
        // LMsk, Lr16, Lr32, Layr, Mt16, Mt32, Mtrn, Alph, FMsk, lnk2, FEid, FXid, PxSD
        if (header.largeFormat) {
            switch (resId) {
                case PSD.LMsk:
                case PSD.Lr16:
                case PSD.Lr32:
                case PSD.Layr:
                case PSD.Mt16:
                case PSD.Mt32:
                case PSD.Mtrn:
                case PSD.Alph:
                case PSD.FMsk:
                case PSD.lnk2:
                case PSD.FEid:
                case PSD.FXid:
                case PSD.PxSD:
                    return stream.readLong();
                default:
                    // Fall through to 32 bit length
            }
        }

        return stream.readUnsignedInt();
    }

    private List<PSDLayerInfo> readLayerInfo(int layerCount) throws IOException {
        PSDLayerInfo[] layerInfos = new PSDLayerInfo[layerCount];

        Stack<List<PSDLayerInfo>> groupStack = new Stack<>();
        List<PSDLayerInfo> currentGroup = Collections.emptyList();

        for (int i = 0; i < layerInfos.length; i++) {
            PSDLayerInfo layerInfo = new PSDLayerInfo(header.largeFormat, imageInput);
            layerInfos[i] = layerInfo;

            if (layerInfo.isDivider) {
                groupStack.push(currentGroup);
                currentGroup = new ArrayList<>();
            }
            else if (layerInfo.isGroup) {
                for (PSDLayerInfo info : currentGroup) {
                    info.groupId = layerInfo.getLayerId();
                }

                currentGroup = groupStack.pop();
            }

            if (currentGroup != Collections.EMPTY_LIST) {
                currentGroup.add(layerInfo);
            }
        }

        return Arrays.asList(layerInfos);
    }

    private BufferedImage readLayerData(final int layerIndex, final ImageReadParam param) throws IOException {
        final int width = getLayerWidth(layerIndex);
        final int height = getLayerHeight(layerIndex);

        // TODO: This behaviour must be documented!
        // If layer has no pixel data, return null, as we can't create a 0 x 0 sample model/raster
        if (width <= 0 || height <= 0) {
            return null;
        }

        PSDLayerInfo layerInfo = metadata.layerInfo.get(layerIndex);

        // Even if raw/imageType has no alpha, the layers may still have alpha...
        ImageTypeSpecifier imageType = getRawImageTypeForLayer(layerIndex);
        BufferedImage layer = getDestination(param, getImageTypes(layerIndex + 1), width, height);

        imageInput.seek(findLayerStartPos(layerIndex));

        // Source/destination area
        Rectangle area = new Rectangle(width, height);

        final int xsub = 1;
        final int ysub = 1;

        final WritableRaster raster = layer.getRaster();
        final ColorModel destCM = layer.getColorModel();

        ColorModel sourceCM = imageType.getColorModel();
        final WritableRaster rowRaster = sourceCM.createCompatibleWritableRaster((int) Math.ceil(width / (double) sourceCM.getNumComponents()), 1);

        final boolean banded = raster.getDataBuffer().getNumBanks() > 1;
        final int interleavedBands = banded ? 1 : raster.getNumBands();

        processImageStarted(1 + layerIndex);

        // TODO: Consider creating a method in PSDLayerInfo that can tell how many channels we really want to decode
        for (int channel = 0; channel < layerInfo.channelInfo.length; channel++) {
            PSDChannelInfo channelInfo = layerInfo.channelInfo[channel];

            int compression = imageInput.readUnsignedShort();

            // Skip layer if we can't read it
            // channelId -1 = transparency mask; -2 = user supplied layer mask, -3 = real user supplied layer mask (when both a user mask and a vector mask are present)
            if (channelInfo.channelId < -1) {
                processWarningOccurred(String.format("Skipping channel %s (%s)", channelInfo.channelId, channelInfo.channelId >= -3 ? "user supplied layer mask" : "unknown channel data"));
                imageInput.skipBytes(channelInfo.length - 2);
            } else {
                // 0 = red, 1 = green, etc -1 = transparency mask
                int band = channelInfo.channelId == -1 ? rowRaster.getNumBands() - 1 : channelInfo.channelId;

                // NOTE: For layers, byte counts are written per channel, while for the composite data
                //       byte counts are written for all channels before the image data.
                //       This is the reason for the current code duplication
                int[] byteCounts = null;

                // 0: None, 1: PackBits RLE, 2: Zip, 3: Zip w/prediction
                switch (compression) {
                    case PSD.COMPRESSION_NONE:
                    case PSD.COMPRESSION_ZIP:
                    case PSD.COMPRESSION_ZIP_PREDICTION:
                        break;
                    case PSD.COMPRESSION_RLE:
                        // If RLE, the image data starts with the byte counts
                        // for all the scan lines in the channel (LayerBottom-LayerTop), with
                        // each count stored as a two-byte (four for PSB) value.
                        byteCounts = new int[layerInfo.bottom - layerInfo.top];
                        for (int i = 0; i < byteCounts.length; i++) {
                            byteCounts[i] = header.largeFormat ? imageInput.readInt() : imageInput.readUnsignedShort();
                        }

                        break;
                    default:
                        // Explicitly skipped above
                        throw new AssertionError(String.format("Unsupported layer data. Compression: %d", compression));
                }

                try (ImageInputStream stream = createDecompressorStream(imageInput, compression, width, header.bits, byteCounts, channelInfo.length - 2)) {
                    int bandOffset = banded ? 0 : interleavedBands - 1 - band;

                    switch (header.bits) {
                        case 1:
                            byte[] row1 = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
                            read1bitChannel(stream, channel, raster.getDataBuffer(), row1, area, area, xsub, ysub, width, height);
                            break;
                        case 8:
                            byte[] row8 = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
                            read8bitChannel(stream, channel, imageType.getNumBands(), raster.getDataBuffer(), band, interleavedBands, bandOffset, sourceCM, row8, area, area, xsub, ysub, width, height);
                            break;
                        case 16:
                            short[] row16 = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
                            read16bitChannel(stream, channel, imageType.getNumBands(), raster.getDataBuffer(), band, interleavedBands, bandOffset, sourceCM, row16, area, area, xsub, ysub, width, height);
                            break;
                        case 32:
                            int[] row32 = ((DataBufferInt) rowRaster.getDataBuffer()).getData();
                            read32bitChannel(stream, channel, imageType.getNumBands(), raster.getDataBuffer(), band, interleavedBands, bandOffset, sourceCM, row32, area, area, xsub, ysub, width, height);
                            break;
                        default:
                            throw new IIOException(String.format("Unknown PSD bit depth: %s", header.bits));
                    }

                    if (abortRequested()) {
                        break;
                    }
                }
            }
        }

        if (!sourceCM.getColorSpace().equals(destCM.getColorSpace())) {
            convertToDestinationCS(sourceCM, destCM, raster);
        }

        processImageComplete();

        return layer;
    }

    private ImageTypeSpecifier getRawImageTypeForLayer(final int layerIndex) throws IOException {
        ImageTypeSpecifier compositeType = getRawImageTypeForCompositeLayer();

        PSDLayerInfo layerInfo = metadata.layerInfo.get(layerIndex);

        // If layer has more channels than composite data, it's normally extra alpha...
        if (layerInfo.channelInfo.length > compositeType.getNumBands()) {
            // ...but, it could also be just one of the user masks...
            int newBandNum = 0;

            for (PSDChannelInfo channelInfo : layerInfo.channelInfo) {
                // -2 = user supplied layer mask, -3 real user supplied layer mask (when both a user mask and a vector mask are present)
                if (channelInfo.channelId >= -1) {
                    newBandNum++;
                }
            }

            // If there really is more channels, then create new imageTypeSpec
            if (newBandNum > compositeType.getNumBands()) {
                int[] indices = new int[newBandNum];
                for (int i = 0, indicesLength = indices.length; i < indicesLength; i++) {
                    indices[i] = i;
                }

                int[] offs = new int[newBandNum];
                for (int i = 0, offsLength = offs.length; i < offsLength; i++) {
                    offs[i] = 0;
                }

                return ImageTypeSpecifiers.createBanded(compositeType.getColorModel().getColorSpace(), indices, offs, compositeType.getSampleModel().getDataType(), true, false);
            }
        }

        return compositeType;
    }

    /// Layer support

    @Override
    public int getNumImages(boolean allowSearch) throws IOException {
        // NOTE: Spec says this method should throw IllegalStateException if allowSearch && isSeekForwardOnly()
        // But that makes no sense for a format (like PSD) that does not need to search, right?
        readLayerAndMaskInfo(false);

        // TODO: Do we really want to include the layers that doesn't have pixel data?
        return metadata.getLayerCount() + 1; // TODO: Only plus one, if "has real merged data"?
    }

    /// Metadata support

    @Override
    public IIOMetadata getStreamMetadata() throws IOException {
        // null might be appropriate here
        // "For image formats that contain a single image, only image metadata is used."
        return super.getStreamMetadata();
    }

    @Override
    public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
        checkBounds(imageIndex);

        readImageResources(true);
        readLayerAndMaskInfo(true);

        // NOTE: Need to make sure compression is set in metadata, even without reading the image data!
        // TODO: Move this to readLayerAndMaskInfo?
        if (metadata.compression == -1) {
            imageInput.seek(metadata.imageDataStart);
            metadata.compression = imageInput.readShort();
        }

        // Initialize XMP data etc.
        for (PSDImageResource resource : metadata.imageResources) {
            if (resource instanceof PSDDirectoryResource) {
                PSDDirectoryResource directoryResource = (PSDDirectoryResource) resource;

                try {
                    directoryResource.initDirectory();
                }
                catch (IOException e) {
                    processWarningOccurred(String.format("Error parsing %s: %s", resource.getClass().getSimpleName(), e.getMessage()));
                }
            }
        }

        return metadata; // TODO: clone if we change to mutable metadata
    }

    @Override
    public IIOMetadata getImageMetadata(final int imageIndex, final String formatName, final Set<String> nodeNames) throws IOException {
        // TODO: It might make sense to overload this, as there's loads of meta data in the file
        return super.getImageMetadata(imageIndex, formatName, nodeNames);
    }

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

    private List<PSDThumbnail> getThumbnailResources(final int imageIndex) throws IOException {
        checkBounds(imageIndex);

        if (imageIndex > 0) {
            return null;
        }

        readHeader();

        List<PSDThumbnail> thumbnails = null;

        if (metadata.imageResources == null) {
            // TODO: Need flag here, to specify what resources to read...
            readImageResources(true);
        }

        for (PSDImageResource resource : metadata.imageResources) {
            if (resource instanceof PSDThumbnail) {
                if (thumbnails == null) {
                    thumbnails = new ArrayList<>();
                }

                thumbnails.add((PSDThumbnail) resource);
            }
        }

        return thumbnails;
    }

    @Override
    public int getNumThumbnails(final int imageIndex) throws IOException {
        List<PSDThumbnail> thumbnails = getThumbnailResources(imageIndex);

        return thumbnails == null ? 0 : thumbnails.size();
    }

    private PSDThumbnail getThumbnailResource(final int imageIndex, final int thumbnailIndex) throws IOException {
        List<PSDThumbnail> thumbnails = getThumbnailResources(imageIndex);

        if (thumbnails == null) {
            throw new IndexOutOfBoundsException(String.format("image index %d > 0", imageIndex));
        }

        return thumbnails.get(thumbnailIndex);
    }

    @Override
    public int getThumbnailWidth(final int imageIndex, final int thumbnailIndex) throws IOException {
        return getThumbnailResource(imageIndex, thumbnailIndex).getWidth();
    }

    @Override
    public int getThumbnailHeight(final int imageIndex, final int thumbnailIndex) throws IOException {
        return getThumbnailResource(imageIndex, thumbnailIndex).getHeight();
    }

    @Override
    public BufferedImage readThumbnail(final int imageIndex, final int thumbnailIndex) throws IOException {
        PSDThumbnail thumbnail = getThumbnailResource(imageIndex, thumbnailIndex);

        // TODO: It's possible to attach listeners to the ImageIO reader delegate... But do we really care?
        processThumbnailStarted(imageIndex, thumbnailIndex);
        processThumbnailProgress(0);
        BufferedImage image = thumbnail.getThumbnail();
        processThumbnailProgress(100);
        processThumbnailComplete();

        return image;
    }

    /// Functional testing
    public static void main(final String[] pArgs) throws IOException {
        int subsampleFactor = 1;
        Rectangle sourceRegion = null;
        boolean readLayers = false;
        boolean readThumbnails = false;

        int idx = 0;
        while (pArgs[idx].charAt(0) == '-') {
            switch (pArgs[idx]) {
                case "-s":
                case "--subsampling":
                    subsampleFactor = Integer.parseInt(pArgs[++idx]);
                    break;
                case "-r":
                case "--sourceregion":
                    int xw = Integer.parseInt(pArgs[++idx]);
                    int yh = Integer.parseInt(pArgs[++idx]);

                    try {
                        int w = Integer.parseInt(pArgs[idx + 1]);
                        int h = Integer.parseInt(pArgs[idx + 2]);

                        idx += 2;

                        // x y w h
                        sourceRegion = new Rectangle(xw, yh, w, h);
                    }
                    catch (NumberFormatException e) {
                        // w h
                        sourceRegion = new Rectangle(xw, yh);
                    }

                    System.out.println("sourceRegion: " + sourceRegion);
                    break;
                case "-l":
                case "--layers":
                    readLayers = true;
                    break;
                case "-t":
                case "--thumbnails":
                    readThumbnails = true;
                    break;
                default:
                    System.err.println("Usage: java PSDImageReader [-s <subsample factor>] [-r [<x y>] <w h>] [-t -l] <image file>");
                    System.exit(1);
            }

            idx++;
        }

        PSDImageReader imageReader = new PSDImageReader(null);

        for (; idx < pArgs.length; idx++) {
            File file = new File(pArgs[idx]);
            System.out.println();
            System.out.println("file: " + file.getAbsolutePath());

            ImageInputStream stream = ImageIO.createImageInputStream(file);
            imageReader.setInput(stream);
            imageReader.readHeader();
            System.out.println("imageReader.header: " + imageReader.header);

            imageReader.readImageResources(true);
            System.out.println("imageReader.imageResources: " + imageReader.metadata.imageResources);
            System.out.println();

            imageReader.readLayerAndMaskInfo(true);
            System.out.println("imageReader.layerInfo: " + imageReader.metadata.layerInfo);
/*
    //        System.out.println("imageReader.globalLayerMask: " + imageReader.globalLayerMask);
            System.out.println();

            IIOMetadata metadata = imageReader.getImageMetadata(0);
            Node node;
            XMLSerializer serializer;

            node = metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
            serializer = new XMLSerializer(System.out, System.getProperty("file.encoding"));
            serializer.serialize(node, true);
            System.out.println();

            node = metadata.getAsTree(PSDMetadata.NATIVE_METADATA_FORMAT_NAME);
    //        serializer = new XMLSerializer(System.out, System.getProperty("file.encoding"));
            serializer.serialize(node, true);
*/
            if (readThumbnails && imageReader.hasThumbnails(0)) {
                int thumbnails = imageReader.getNumThumbnails(0);
                for (int i = 0; i < thumbnails; i++) {
                    showIt(imageReader.readThumbnail(0, i), String.format("Thumbnail %d", i));
                }
            }

            long start = System.currentTimeMillis();

            ImageReadParam param = imageReader.getDefaultReadParam();

            if (sourceRegion != null) {
                param.setSourceRegion(sourceRegion);
            }

            if (subsampleFactor > 1) {
                param.setSourceSubsampling(subsampleFactor, subsampleFactor, 0, 0);
            }

            //        param.setDestinationType(imageReader.getRawImageType(0));

            BufferedImage image = imageReader.read(0, param);
            System.out.println("read time: " + (System.currentTimeMillis() - start));
            System.out.println("image: " + image);

            if (image.getType() == BufferedImage.TYPE_CUSTOM) {
                try {
                    ColorConvertOp op = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_sRGB), null);
                    GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
                    image = op.filter(image, gc.createCompatibleImage(image.getWidth(), image.getHeight(), image.getTransparency()));
                }
                catch (Exception e) {
                    e.printStackTrace();
                    image = ImageUtil.accelerate(image);
                }
                System.out.println("conversion time: " + (System.currentTimeMillis() - start));
                System.out.println("image: " + image);
            }

            showIt(image, file.getName());

            if (readLayers) {
                int images = imageReader.getNumImages(true);
                for (int i = 1; i < images; i++) {
                    start = System.currentTimeMillis();
                    BufferedImage layer = imageReader.read(i);

                    System.out.println("layer read time: " + (System.currentTimeMillis() - start));
                    System.err.println("layer: " + layer);

                    if (layer != null && layer.getType() == BufferedImage.TYPE_CUSTOM) {
                        try {
                            ColorConvertOp op = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_sRGB), null);
                            GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
                            layer = op.filter(layer, gc.createCompatibleImage(layer.getWidth(), layer.getHeight(), layer.getTransparency()));
                        }
                        catch (Exception e) {
                            e.printStackTrace();
                            layer = ImageUtil.accelerate(layer);
                        }
                        System.out.println("layer conversion time: " + (System.currentTimeMillis() - start));
                        System.out.println("layer: " + layer);
                    }

                    showIt(layer, "layer " + i);
                }
            }
        }
    }
}
