/*
 * 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 "TwelveMonkeys" 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 OWNER 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.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.*;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.List;

/**
 * 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: Implement ImageIO meta data interface
// 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)
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 && header.bits == 8) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0}, new int[] {0}, DataBuffer.TYPE_BYTE, false, false);
                }
                else if (header.channels == 2 && header.bits == 8) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1}, new int[] {0, 0}, DataBuffer.TYPE_BYTE, true, false);
                }
                else if (header.channels == 1 && header.bits == 16) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0}, new int[] {0}, DataBuffer.TYPE_USHORT, false, false);
                }
                else if (header.channels == 2 && header.bits == 16) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1}, new int[] {0, 0}, DataBuffer.TYPE_USHORT, true, false);
                }
                else if (header.channels == 1 && header.bits == 32) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0}, new int[] {0}, DataBuffer.TYPE_INT, false, false);
                }
                else if (header.channels == 2 && header.bits == 32) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1}, new int[] {0, 0}, DataBuffer.TYPE_INT, true, 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 && header.bits == 8) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2}, new int[] {0, 0, 0}, DataBuffer.TYPE_BYTE, false, false);
                }
                else if (header.channels >= 4 && header.bits == 8) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_BYTE, true, false);
                }
                else if (header.channels == 3 && header.bits == 16) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2}, new int[] {0, 0, 0}, DataBuffer.TYPE_USHORT, false, false);
                }
                else if (header.channels >= 4 && header.bits == 16) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_USHORT, true, false);
                }
                else if (header.channels == 3 && header.bits == 32) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2}, new int[] {0, 0, 0}, DataBuffer.TYPE_INT, false, false);
                }
                else if (header.channels >= 4 && header.bits == 32) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_INT, true, 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 && header.bits == 8) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_BYTE, false, false);
                }
                else if (header.channels == 5 && header.bits == 8) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, DataBuffer.TYPE_BYTE, true, false);
                }
                else if (header.channels == 4 && header.bits == 16) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_USHORT, false, false);
                }
                else if (header.channels == 5 && header.bits == 16) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, DataBuffer.TYPE_USHORT, true, false);
                }
                else if (header.channels == 4 && header.bits == 32) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3}, new int[] {0, 0, 0, 0}, DataBuffer.TYPE_INT, false, false);
                }
                else if (header.channels == 5 && header.bits == 32) {
                    return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2, 3, 4}, new int[] {0, 0, 0, 0, 0}, DataBuffer.TYPE_INT, true, 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_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 i = 0; i < byteCounts.length; i++) {
                    byteCounts[i] = header.largeFormat ? imageInput.readInt() : imageInput.readUnsignedShort();
                }
                break;
            case PSD.COMPRESSION_ZIP:
            case PSD.COMPRESSION_ZIP_PREDICTION:
                // TODO: Could probably use the ZIPDecoder (DeflateDecoder) here.. Look at TIFF prediction reading
                throw new IIOException("PSD with ZIP compression not supported");
            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[] pByteCounts, final int pCompression) 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++) {
            int bandOffset = banded ? 0 : interleavedBands - 1 - c;

            switch (header.bits) {
                case 1:
                    byte[] row1 = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
                    read1bitChannel(c, header.channels, destRaster.getDataBuffer(), interleavedBands, bandOffset, pSourceCM, row1, pSource, pDest, pXSub, pYSub, header.width, header.height, pByteCounts, pCompression == PSD.COMPRESSION_RLE);
                    break;
                case 8:
                    byte[] row8 = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
                    read8bitChannel(c, header.channels, destRaster.getDataBuffer(), interleavedBands, bandOffset, pSourceCM, row8, pSource, pDest, pXSub, pYSub, header.width, header.height, pByteCounts, c * header.height, pCompression == PSD.COMPRESSION_RLE);
                    break;
                case 16:
                    short[] row16 = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
                    read16bitChannel(c, header.channels, destRaster.getDataBuffer(), interleavedBands, bandOffset, pSourceCM, row16, pSource, pDest, pXSub, pYSub, header.width, header.height, pByteCounts, c * header.height, pCompression == PSD.COMPRESSION_RLE);
                    break;
                case 32:
                    int[] row32 = ((DataBufferInt) rowRaster.getDataBuffer()).getData();
                    read32bitChannel(c, header.channels, destRaster.getDataBuffer(), interleavedBands, bandOffset, pSourceCM, row32, pSource, pDest, pXSub, pYSub, header.width, header.height, pByteCounts, c * header.height, pCompression == PSD.COMPRESSION_RLE);
                    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(destination.getColorModel().getColorSpace())) {
            convertToDestinationCS(pSourceCM, destination.getColorModel(), 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 int pChannel, final int pChannelCount,
                                  final DataBuffer pData, final int pBands, final int pBandOffset,
                                  final ColorModel pSourceColorModel,
                                  final int[] pRow,
                                  final Rectangle pSource, final Rectangle pDest,
                                  final int pXSub, final int pYSub,
                                  final int pChannelWidth, final int pChannelHeight,
                                  final int[] pRowByteCounts, final int pRowOffset,
                                  final boolean pRLECompressed) throws IOException {

        boolean isCMYK = pSourceColorModel.getColorSpace().getType() == ColorSpace.TYPE_CMYK;
        int colorComponents = pSourceColorModel.getColorSpace().getNumComponents();
        final boolean invert = isCMYK && pChannel < colorComponents;
        final boolean banded = pData.getNumBanks() > 1;

        for (int y = 0; y < pChannelHeight; y++) {
            int length = (pRLECompressed ? pRowByteCounts[pRowOffset + y] : 4 * pChannelWidth);

            // TODO: Sometimes need to read the line y == source.y + source.height...
            // Read entire line, if within source region and sampling
            if (y >= pSource.y && y < pSource.y + pSource.height && y % pYSub == 0) {
                if (pRLECompressed) {

                    try (DataInputStream input = PSDUtil.createPackBitsStream(imageInput, length)) {
                        for (int x = 0; x < pChannelWidth; x++) {
                            pRow[x] = input.readInt();
                        }
                    }
                }
                else {
                    imageInput.readFully(pRow, 0, pChannelWidth);
                }

                // TODO: Destination offset...??
                // Copy line sub sampled into real data
                int offset = (y - pSource.y) / pYSub * pDest.width * pBands + pBandOffset;
                for (int x = 0; x < pDest.width; x++) {
                    int value = pRow[pSource.x + x * pXSub];

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

                    pData.setElem(banded ? pChannel : 0, offset + x * pBands, value);
                }
            }
            else {
                imageInput.skipBytes(length);
            }

            if (abortRequested()) {
                break;
            }

            processImageProgressForChannel(pChannel, pChannelCount, y, pChannelHeight);
        }
    }

    private void read16bitChannel(final int pChannel, final int pChannelCount,
                                  final DataBuffer pData, final int pBands, final int pBandOffset,
                                  final ColorModel pSourceColorModel,
                                  final short[] pRow,
                                  final Rectangle pSource, final Rectangle pDest,
                                  final int pXSub, final int pYSub,
                                  final int pChannelWidth, final int pChannelHeight,
                                  final int[] pRowByteCounts, final int pRowOffset,
                                  final boolean pRLECompressed) throws IOException {

        boolean isCMYK = pSourceColorModel.getColorSpace().getType() == ColorSpace.TYPE_CMYK;
        int colorComponents = pSourceColorModel.getColorSpace().getNumComponents();
        final boolean invert = isCMYK && pChannel < colorComponents;
        final boolean banded = pData.getNumBanks() > 1;

        for (int y = 0; y < pChannelHeight; y++) {
            int length = (pRLECompressed ? pRowByteCounts[pRowOffset + y] : 2 * pChannelWidth);

            // TODO: Sometimes need to read the line y == source.y + source.height...
            // Read entire line, if within source region and sampling
            if (y >= pSource.y && y < pSource.y + pSource.height && y % pYSub == 0) {
                if (pRLECompressed) {
                    try (DataInputStream input = PSDUtil.createPackBitsStream(imageInput, length)) {
                        for (int x = 0; x < pChannelWidth; x++) {
                            pRow[x] = input.readShort();
                        }
                    }
                }
                else {
                    imageInput.readFully(pRow, 0, pChannelWidth);
                }

                // TODO: Destination offset...??
                // Copy line sub sampled into real data
                int offset = (y - pSource.y) / pYSub * pDest.width * pBands + pBandOffset;
                for (int x = 0; x < pDest.width; x++) {
                    short value = pRow[pSource.x + x * pXSub];

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

                    pData.setElem(banded ? pChannel : 0, offset + x * pBands, value);
                }
            }
            else {
                imageInput.skipBytes(length);
            }

            if (abortRequested()) {
                break;
            }

            processImageProgressForChannel(pChannel, pChannelCount, y, pChannelHeight);
        }
    }

    private void read8bitChannel(final int pChannel, final int pChannelCount,
                                 final DataBuffer pData, final int pBands, final int pBandOffset,
                                 final ColorModel pSourceColorModel,
                                 final byte[] pRow,
                                 final Rectangle pSource, final Rectangle pDest,
                                 final int pXSub, final int pYSub,
                                 final int pChannelWidth, final int pChannelHeight,
                                 final int[] pRowByteCounts, final int pRowOffset,
                                 final boolean pRLECompressed) throws IOException {

        boolean isCMYK = pSourceColorModel.getColorSpace().getType() == ColorSpace.TYPE_CMYK;
        int colorComponents = pSourceColorModel.getColorSpace().getNumComponents();
        final boolean invert = isCMYK && pChannel < colorComponents;
        final boolean banded = pData.getNumBanks() > 1;

        for (int y = 0; y < pChannelHeight; y++) {
            int length = pRLECompressed ? pRowByteCounts[pRowOffset + y] : pChannelWidth;

            // TODO: Sometimes need to read the line y == source.y + source.height...
            // Read entire line, if within source region and sampling
            if (y >= pSource.y && y < pSource.y + pSource.height && y % pYSub == 0) {
                if (pRLECompressed) {
                    try (DataInputStream input = PSDUtil.createPackBitsStream(imageInput, length)) {
                        input.readFully(pRow, 0, pChannelWidth);
                    }
                }
                else {
                    imageInput.readFully(pRow, 0, pChannelWidth);
                }

                // TODO: Destination offset...??
                // Copy line sub sampled into real data
                int offset = (y - pSource.y) / pYSub * pDest.width * pBands + pBandOffset;
                for (int x = 0; x < pDest.width; x++) {
                    byte value = pRow[pSource.x + x * pXSub];

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

                    pData.setElem(banded ? pChannel : 0, offset + x * pBands, value);
                }
            }
            else {
                imageInput.skipBytes(length);
            }

            if (abortRequested()) {
                break;
            }

            processImageProgressForChannel(pChannel, pChannelCount, y, pChannelHeight);
        }
    }

    @SuppressWarnings({"UnusedDeclaration"})
    private void read1bitChannel(final int pChannel, final int pChannelCount,
                                 final DataBuffer pData, final int pBands, final int pBandOffset,
                                 final ColorModel pSourceColorModel,
                                 final byte[] pRow,
                                 final Rectangle pSource, final Rectangle pDest,
                                 final int pXSub, final int pYSub,
                                 final int pChannelWidth, final int pChannelHeight,
                                 final int[] pRowByteCounts, boolean pRLECompressed) throws IOException {
        // NOTE: 1 bit channels only occurs once

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

        for (int y = 0; y < pChannelHeight; y++) {
            int length = pRLECompressed ? pRowByteCounts[y] : pChannelWidth;

            // TODO: Sometimes need to read the line y == source.y + source.height...
            // Read entire line, if within source region and sampling
            if (y >= pSource.y && y < pSource.y + pSource.height && y % pYSub == 0) {
                if (pRLECompressed) {
                    try (DataInputStream input = PSDUtil.createPackBitsStream(imageInput, length)) {
                        input.readFully(pRow, 0, pRow.length);
                    }
                }
                else {
                    imageInput.readFully(pRow, 0, pRow.length);
                }

                // TODO: Destination offset...??
                int offset = (y - pSource.y) / pYSub * destWidth;
                if (pXSub == 1 && pSource.x % 8 == 0) {
                    // Fast normal case, no sub sampling
                    for (int i = 0; i < destWidth; i++) {
                        byte value = pRow[pSource.x / 8 + i * pXSub];
                        // NOTE: Invert bits to match Java's default monochrome
                        pData.setElem(banded ? pChannel : 0, offset + i, (byte) (~value & 0xff));
                    }
                }
                else {
                    // Copy line sub sampled into real data
                    final int maxX = pSource.x + pSource.width;
                    int x = pSource.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 |= ((pRow[bytePos] & mask) >> sourceBitOff) << destBitOff;

                            x += pXSub;
                        }

                        // NOTE: Invert bits to match Java's default monochrome
                        pData.setElem(banded ? pChannel : 0, offset + i, (byte) (~result & 0xff));
                    }
                }
            }
            else {
                imageInput.skipBytes(length);
            }

            if (abortRequested()) {
                break;
            }

            processImageProgressForChannel(pChannel, pChannelCount, y, pChannelHeight);
        }
    }

    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 = new PSDHeader(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.layerAndMaskInfoStart == 0) {
            imageInput.seek(metadata.imageResourcesStart);

            long imageResourcesLength = imageInput.readUnsignedInt();

            if (pParseData && imageResourcesLength > 0) {
                if (metadata.imageResources == null) {
                    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 pParseData) throws IOException {
        readImageResources(false);

        if (pParseData || metadata.imageDataStart == 0) {
            imageInput.seek(metadata.layerAndMaskInfoStart);

            long layerAndMaskInfoLength = header.largeFormat ? imageInput.readLong() : imageInput.readUnsignedInt();

            // 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 alo 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 (pParseData && layerAndMaskInfoLength > 0) {
                long pos = imageInput.getStreamPosition();

                if (metadata.layerInfo == null) {
                    long layerInfoLength = header.largeFormat ? imageInput.readLong() : imageInput.readUnsignedInt();

                    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();

                        PSDLayerInfo[] layerInfos = new PSDLayerInfo[Math.abs(layerCount)];
                        for (int i = 0; i < layerInfos.length; i++) {
                            layerInfos[i] = new PSDLayerInfo(header.largeFormat, imageInput);
                        }
                        metadata.layerInfo = Arrays.asList(layerInfos);
                        metadata.layersStart = imageInput.getStreamPosition();

                        long read = imageInput.getStreamPosition() - pos;

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

                        imageInput.skipBytes(diff);
                    }
                    else {
                        metadata.layerInfo = Collections.emptyList();
                    }

                    // 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) {
                        metadata.globalLayerMask = new PSDGlobalLayerMask(imageInput, globalLayerMaskInfoLength);
                    }

                    // TODO: Parse "Additional layer information"

                    // 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 (DEBUG) {
                        System.out.println("layerInfo: " + metadata.layerInfo);
                    }
                }
            }

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

    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
        if (width <= 0 || height <= 0) {
            return null;
        }

        PSDLayerInfo layerInfo = metadata.layerInfo.get(layerIndex);
//        final int width = layerInfo.right - layerInfo.left;
//        final int height = layerInfo.bottom - layerInfo.top;

        // Even if raw/imageType has no alpha, the layers may still have alpha...
        ImageTypeSpecifier imageType = getRawImageTypeForLayer(layerIndex);
        BufferedImage layer = getDestination(param, getImageTypes(layerIndex + 1), Math.max(1, width), Math.max(1, 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();

        // TODO: progress for layers!
        // TODO: Consider creating a method in PSDLayerInfo that can tell how many channels we really want to decode
        for (PSDChannelInfo channelInfo : layerInfo.channelInfo) {
            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 (width <= 0 || height <= 0 || channelInfo.channelId < -1 ||
                    (compression != PSD.COMPRESSION_NONE && compression != PSD.COMPRESSION_RLE)) { // TODO: ZIP Compressions!
                imageInput.skipBytes(channelInfo.length - 2);
            }
            else {
                // 0 = red, 1 = green, etc
                // -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)
                int c = 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:
                        break;
                    case PSD.COMPRESSION_RLE:
                        // If RLE, the 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;
                    case PSD.COMPRESSION_ZIP:
                    case PSD.COMPRESSION_ZIP_PREDICTION:
                    default:
                        // Explicitly skipped above
                        throw new AssertionError(String.format("Unsupported layer data. Compression: %d", compression));
                }

                int bandOffset = banded ? 0 : interleavedBands - 1 - c;

                switch (header.bits) {
                    case 1:
                        byte[] row1 = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
                        read1bitChannel(c, imageType.getNumBands(), raster.getDataBuffer(), interleavedBands, bandOffset, sourceCM, row1, area, area, xsub, ysub, width, height, byteCounts, compression == PSD.COMPRESSION_RLE);
                        break;
                    case 8:
                        byte[] row8 = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
                        read8bitChannel(c, imageType.getNumBands(), raster.getDataBuffer(), interleavedBands, bandOffset, sourceCM, row8, area, area, xsub,
                                        ysub, width, height, byteCounts, 0, compression == PSD.COMPRESSION_RLE);
                        break;
                    case 16:
                        short[] row16 = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
                        read16bitChannel(c, imageType.getNumBands(), raster.getDataBuffer(), interleavedBands, bandOffset, sourceCM, row16, area, area, xsub,
                                         ysub, width, height, byteCounts, 0, compression == PSD.COMPRESSION_RLE);
                        break;
                    case 32:
                        int[] row32 = ((DataBufferInt) rowRaster.getDataBuffer()).getData();
                        read32bitChannel(c, imageType.getNumBands(), raster.getDataBuffer(), interleavedBands, bandOffset, sourceCM, row32, area, area, xsub,
                                         ysub, width, height, byteCounts, 0, compression == PSD.COMPRESSION_RLE);
                        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);
        }

        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
    protected void checkBounds(final int index) throws IOException {
        // Avoid parsing layer stuff, if we just want to read the composite data
        if (index == 0) {
            assertInput();
            readLayerAndMaskInfo(false);
        }
        else {
            super.checkBounds(index);
        }
    }

    @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(true);

        return metadata.layerInfo != null ? metadata.layerInfo.size() + 1 : 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!
        imageInput.seek(metadata.imageDataStart);
        metadata.compression = imageInput.readShort();

        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);

        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("thumbnail index %d > 0", thumbnailIndex));
        }

        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 {
        // TODO: Thumbnail progress listeners...
        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) == '-') {
            if (pArgs[idx].equals("-s") || pArgs[idx].equals("--subsampling")) {
                subsampleFactor = Integer.parseInt(pArgs[++idx]);
            }
            else if (pArgs[idx].equals("-r") || pArgs[idx].equals("--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);
            }
            else if (pArgs[idx].equals("-l") || pArgs[idx].equals("--layers")) {
                readLayers = true;
            }
            else if (pArgs[idx].equals("-t") || pArgs[idx].equals("--thumbnails")) {
                readThumbnails = true;
            }
            else {
                System.err.println("Usage: java PSDImageReader [-s <subsample factor>] [-r [<x y>] <w h>] <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);
                }
            }
        }
    }
}
