/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* $Id: GraphicsUtil.java 1732018 2016-02-24 04:51:06Z gadams $ */

package org.apache.xmlgraphics.image;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.color.ColorSpace;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentSampleModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.awt.image.DataBufferShort;
import java.awt.image.DataBufferUShort;
import java.awt.image.DirectColorModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.SinglePixelPackedSampleModel;
import java.awt.image.WritableRaster;

import org.apache.xmlgraphics.image.rendered.Any2LsRGBRed;
import org.apache.xmlgraphics.image.rendered.Any2sRGBRed;
import org.apache.xmlgraphics.image.rendered.BufferedImageCachableRed;
import org.apache.xmlgraphics.image.rendered.CachableRed;
import org.apache.xmlgraphics.image.rendered.RenderedImageCachableRed;

// CSOFF: AvoidNestedBlocks
// CSOFF: ConstantName
// CSOFF: MethodName
// CSOFF: MultipleVariableDeclarations
// CSOFF: NeedBraces
// CSOFF: OneStatementPerLine
// CSOFF: OperatorWrap
// CSOFF: StaticVariableName
// CSOFF: WhitespaceAfter
// CSOFF: WhitespaceAround

/**
 * Set of utility methods for Graphics.
 * These generally bypass broken methods in Java2D or provide tweaked
 * implementations.
 *
 * @version $Id: GraphicsUtil.java 1732018 2016-02-24 04:51:06Z gadams $
 *
 * Originally authored by Thomas DeWeese.
 */
public final class GraphicsUtil {

    private GraphicsUtil() {
    }

    public static final AffineTransform IDENTITY = new AffineTransform();

    /**
     * Standard prebuilt Linear_sRGB color model with no alpha */
    public static final ColorModel Linear_sRGB =
        new DirectColorModel(ColorSpace.getInstance(
                             ColorSpace.CS_LINEAR_RGB), 24,
                             0x00FF0000, 0x0000FF00,
                             0x000000FF, 0x0, false,
                             DataBuffer.TYPE_INT);
    /**
     * Standard prebuilt Linear_sRGB color model with premultiplied alpha.
     */
    public static final ColorModel Linear_sRGB_Pre =
        new DirectColorModel(ColorSpace.getInstance(
                             ColorSpace.CS_LINEAR_RGB), 32,
                             0x00FF0000, 0x0000FF00,
                             0x000000FF, 0xFF000000, true,
                             DataBuffer.TYPE_INT);
    /**
     * Standard prebuilt Linear_sRGB color model with unpremultiplied alpha.
     */
    public static final ColorModel Linear_sRGB_Unpre =
        new DirectColorModel(ColorSpace.getInstance(
                             ColorSpace.CS_LINEAR_RGB), 32,
                             0x00FF0000, 0x0000FF00,
                             0x000000FF, 0xFF000000, false,
                             DataBuffer.TYPE_INT);

    /**
     * Standard prebuilt sRGB color model with no alpha.
     */
    public static final ColorModel sRGB =
        new DirectColorModel(ColorSpace.getInstance(
                             ColorSpace.CS_sRGB), 24,
                             0x00FF0000, 0x0000FF00,
                             0x000000FF, 0x0, false,
                             DataBuffer.TYPE_INT);
    /**
     * Standard prebuilt sRGB color model with premultiplied alpha.
     */
    public static final ColorModel sRGB_Pre =
        new DirectColorModel(ColorSpace.getInstance(
                             ColorSpace.CS_sRGB), 32,
                             0x00FF0000, 0x0000FF00,
                             0x000000FF, 0xFF000000, true,
                             DataBuffer.TYPE_INT);
    /**
     * Standard prebuilt sRGB color model with unpremultiplied alpha.
     */
    public static final ColorModel sRGB_Unpre =
        new DirectColorModel(ColorSpace.getInstance(
                             ColorSpace.CS_sRGB), 32,
                             0x00FF0000, 0x0000FF00,
                             0x000000FF, 0xFF000000, false,
                             DataBuffer.TYPE_INT);

    /**
     * Method that returns either Linear_sRGB_Pre or Linear_sRGB_UnPre
     * based on premult flag.
     * @param premult True if the ColorModel should have premultiplied alpha.
     * @return        a ColorMdoel with Linear sRGB colorSpace and
     *                the alpha channel set in accordance with
     *                <tt>premult</tt>
     */
    public static ColorModel makeLinear_sRGBCM(boolean premult) {
         return premult ? Linear_sRGB_Pre : Linear_sRGB_Unpre;
    }

    /**
     * Constructs a BufferedImage with a linear sRGB colorModel, and alpha.
     * @param width   The desired width of the BufferedImage
     * @param height  The desired height of the BufferedImage
     * @param premult The desired state of alpha premultiplied
     * @return        The requested BufferedImage.
     */
    public static BufferedImage makeLinearBufferedImage(int width,
                                                        int height,
                                                        boolean premult) {
        ColorModel cm = makeLinear_sRGBCM(premult);
        WritableRaster wr = cm.createCompatibleWritableRaster(width, height);
        return new BufferedImage(cm, wr, premult, null);
    }

    /**
     * This method will return a CacheableRed that has it's data in
     * the linear sRGB colorspace. If <tt>src</tt> is already in
     * linear sRGB then this method does nothing and returns <tt>src</tt>.
     * Otherwise it creates a transform that will convert
     * <tt>src</tt>'s output to linear sRGB and returns that CacheableRed.
     *
     * @param src The image to convert to linear sRGB.
     * @return    An equivilant image to <tt>src</tt> who's data is in
     *            linear sRGB.
     */
    public static CachableRed convertToLsRGB(CachableRed src) {
        ColorModel cm = src.getColorModel();
        ColorSpace cs = cm.getColorSpace();
        if (cs == ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB)) {
            return src;
        }

        return new Any2LsRGBRed(src);
    }

    /**
     * This method will return a CacheableRed that has it's data in
     * the sRGB colorspace. If <tt>src</tt> is already in
     * sRGB then this method does nothing and returns <tt>src</tt>.
     * Otherwise it creates a transform that will convert
     * <tt>src</tt>'s output to sRGB and returns that CacheableRed.
     *
     * @param src The image to convert to sRGB.
     * @return    An equivilant image to <tt>src</tt> who's data is in sRGB.
     */
    public static CachableRed convertTosRGB(CachableRed src) {
        ColorModel cm = src.getColorModel();
        ColorSpace cs = cm.getColorSpace();
        if (cs == ColorSpace.getInstance(ColorSpace.CS_sRGB)) {
            return src;
        }

        return new Any2sRGBRed(src);
    }

    /**
     * Convertes any RenderedImage to a CacheableRed.  <p>
     * If <tt>ri</tt> is already a CacheableRed it casts it down and
     * returns it.<p>
     *
     * In cases where <tt>ri</tt> is not already a CacheableRed it
     * wraps <tt>ri</tt> with a helper class.  The wrapped
     * CacheableRed "Pretends" that it has no sources since it has no
     * way of inteligently handling the dependency/dirty region calls
     * if it exposed the source.
     * @param ri The RenderedImage to convert.
     * @return   a CacheableRed that contains the same data as ri.
     */
    public static CachableRed wrap(RenderedImage ri) {
        if (ri instanceof CachableRed) {
            return (CachableRed) ri;
        }
        if (ri instanceof BufferedImage) {
            return new BufferedImageCachableRed((BufferedImage)ri);
        }
        return new RenderedImageCachableRed(ri);
    }

    /**
     * An internal optimized version of copyData designed to work on
     * Integer packed data with a SinglePixelPackedSampleModel.  Only
     * the region of overlap between src and dst is copied.
     *
     * Calls to this should be preflighted with is_INT_PACK_Data
     * on both src and dest (requireAlpha can be false).
     *
     * @param src The source of the data
     * @param dst The destination for the data.
     */
    public static void copyData_INT_PACK(Raster src, WritableRaster dst) {
        // System.out.println("Fast copyData");
        int x0 = dst.getMinX();
        if (x0 < src.getMinX()) {
            x0 = src.getMinX();
        }

        int y0 = dst.getMinY();
        if (y0 < src.getMinY()) {
            y0 = src.getMinY();
        }

        int x1 = dst.getMinX() + dst.getWidth() - 1;
        if (x1 > src.getMinX() + src.getWidth() - 1) {
            x1 = src.getMinX() + src.getWidth() - 1;
        }

        int y1 = dst.getMinY() + dst.getHeight() - 1;
        if (y1 > src.getMinY() + src.getHeight() - 1) {
            y1 = src.getMinY() + src.getHeight() - 1;
        }

        int width  = x1 - x0 + 1;
        int height = y1 - y0 + 1;

        SinglePixelPackedSampleModel srcSPPSM;
        srcSPPSM = (SinglePixelPackedSampleModel)src.getSampleModel();

        final int     srcScanStride = srcSPPSM.getScanlineStride();
        DataBufferInt srcDB         = (DataBufferInt)src.getDataBuffer();
        final int []  srcPixels     = srcDB.getBankData()[0];
        final int     srcBase =
            (srcDB.getOffset()
             + srcSPPSM.getOffset(x0 - src.getSampleModelTranslateX(),
                                y0 - src.getSampleModelTranslateY()));


        SinglePixelPackedSampleModel dstSPPSM;
        dstSPPSM = (SinglePixelPackedSampleModel)dst.getSampleModel();

        final int     dstScanStride = dstSPPSM.getScanlineStride();
        DataBufferInt dstDB         = (DataBufferInt)dst.getDataBuffer();
        final int []  dstPixels     = dstDB.getBankData()[0];
        final int     dstBase =
            (dstDB.getOffset()
             + dstSPPSM.getOffset(x0 - dst.getSampleModelTranslateX(),
                                y0 - dst.getSampleModelTranslateY()));

        if ((srcScanStride == dstScanStride)
            && (srcScanStride == width)) {
            // System.out.println("VERY Fast copyData");

            System.arraycopy(srcPixels, srcBase, dstPixels, dstBase,
                             width * height);
        } else if (width > 128) {
            int srcSP = srcBase;
            int dstSP = dstBase;
            for (int y = 0; y < height; y++) {
                System.arraycopy(srcPixels, srcSP, dstPixels, dstSP, width);
                srcSP += srcScanStride;
                dstSP += dstScanStride;
            }
        } else {
            for (int y = 0; y < height; y++) {
                int srcSP = srcBase + y * srcScanStride;
                int dstSP = dstBase + y * dstScanStride;
                for (int x = 0; x < width; x++) {
                    dstPixels[dstSP++] = srcPixels[srcSP++];
                }
            }
        }
    }

    public static void copyData_FALLBACK(Raster src, WritableRaster dst) {
        // System.out.println("Fallback copyData");

        int x0 = dst.getMinX();
        if (x0 < src.getMinX()) {
            x0 = src.getMinX();
        }

        int y0 = dst.getMinY();
        if (y0 < src.getMinY()) {
            y0 = src.getMinY();
        }

        int x1 = dst.getMinX() + dst.getWidth() - 1;
        if (x1 > src.getMinX() + src.getWidth() - 1) {
            x1 = src.getMinX() + src.getWidth() - 1;
        }

        int y1 = dst.getMinY() + dst.getHeight() - 1;
        if (y1 > src.getMinY() + src.getHeight() - 1) {
            y1 = src.getMinY() + src.getHeight() - 1;
        }

        int width  = x1 - x0 + 1;
        int [] data = null;

        for (int y = y0; y <= y1; y++)  {
            data = src.getPixels(x0, y, width, 1, data);
            dst.setPixels(x0, y, width, 1, data);
        }
    }

    /**
     * Copies data from one raster to another. Only the region of
     * overlap between src and dst is copied.  <tt>Src</tt> and
     * <tt>Dst</tt> must have compatible SampleModels.
     *
     * @param src The source of the data
     * @param dst The destination for the data.
     */
    public static void copyData(Raster src, WritableRaster dst) {
        if (is_INT_PACK_Data(src.getSampleModel(), false)
            && is_INT_PACK_Data(dst.getSampleModel(), false)) {
            copyData_INT_PACK(src, dst);
            return;
        }

        copyData_FALLBACK(src, dst);
    }

    /**
     * Creates a new raster that has a <b>copy</b> of the data in
     * <tt>ras</tt>.  This is highly optimized for speed.  There is
     * no provision for changing any aspect of the SampleModel.
     *
     * This method should be used when you need to change the contents
     * of a Raster that you do not "own" (ie the result of a
     * <tt>getData</tt> call).
     * @param ras The Raster to copy.
     * @return    A writable copy of <tt>ras</tt>
     */
    public static WritableRaster copyRaster(Raster ras) {
        return copyRaster(ras, ras.getMinX(), ras.getMinY());
    }


    /**
     * Creates a new raster that has a <b>copy</b> of the data in
     * <tt>ras</tt>.  This is highly optimized for speed.  There is
     * no provision for changing any aspect of the SampleModel.
     * However you can specify a new location for the returned raster.
     *
     * This method should be used when you need to change the contents
     * of a Raster that you do not "own" (ie the result of a
     * <tt>getData</tt> call).
     *
     * @param ras The Raster to copy.
     *
     * @param minX The x location for the upper left corner of the
     *             returned WritableRaster.
     *
     * @param minY The y location for the upper left corner of the
     *             returned WritableRaster.
     *
     * @return    A writable copy of <tt>ras</tt>
     */
    public static WritableRaster copyRaster(Raster ras, int minX, int minY) {
        WritableRaster ret = Raster.createWritableRaster(
            ras.getSampleModel(),
             new Point(0, 0));
        ret = ret.createWritableChild(
            ras.getMinX() - ras.getSampleModelTranslateX(),
             ras.getMinY() - ras.getSampleModelTranslateY(),
             ras.getWidth(), ras.getHeight(),
             minX, minY, null);

        // Use System.arraycopy to copy the data between the two...
        DataBuffer srcDB = ras.getDataBuffer();
        DataBuffer retDB = ret.getDataBuffer();
        if (srcDB.getDataType() != retDB.getDataType()) {
            throw new IllegalArgumentException(
                "New DataBuffer doesn't match original");
        }
        int len   = srcDB.getSize();
        int banks = srcDB.getNumBanks();
        int [] offsets = srcDB.getOffsets();
        for (int b = 0; b < banks; b++) {
            switch (srcDB.getDataType()) {
            case DataBuffer.TYPE_BYTE: {
                DataBufferByte srcDBT = (DataBufferByte)srcDB;
                DataBufferByte retDBT = (DataBufferByte)retDB;
                System.arraycopy(srcDBT.getData(b), offsets[b],
                                 retDBT.getData(b), offsets[b], len);
                break;
            }
            case DataBuffer.TYPE_INT: {
                DataBufferInt srcDBT = (DataBufferInt)srcDB;
                DataBufferInt retDBT = (DataBufferInt)retDB;
                System.arraycopy(srcDBT.getData(b), offsets[b],
                                 retDBT.getData(b), offsets[b], len);
                break;
            }
            case DataBuffer.TYPE_SHORT: {
                DataBufferShort srcDBT = (DataBufferShort)srcDB;
                DataBufferShort retDBT = (DataBufferShort)retDB;
                System.arraycopy(srcDBT.getData(b), offsets[b],
                                 retDBT.getData(b), offsets[b], len);
                break;
            }
            case DataBuffer.TYPE_USHORT: {
                DataBufferUShort srcDBT = (DataBufferUShort)srcDB;
                DataBufferUShort retDBT = (DataBufferUShort)retDB;
                System.arraycopy(srcDBT.getData(b), offsets[b],
                                 retDBT.getData(b), offsets[b], len);
                break;
            }
            default:
                throw new
                    UnsupportedOperationException("unsupported data type: "
                                                  + srcDB.getDataType());
            }
        }

        return ret;
    }

    /**
     * Coerces <tt>ras</tt> to be writable.  The returned Raster continues to
     * reference the DataBuffer from ras, so modifications to the returned
     * WritableRaster will be seen in ras.<p>
     *
     * This method should only be used if you need a WritableRaster due to
     * an interface (such as to construct a BufferedImage), but have no
     * intention of modifying the contents of the returned Raster.  If
     * you have any doubt about other users of the data in <tt>ras</tt>,
     * use copyRaster (above).
     * @param ras The raster to make writable.
     * @return    A Writable version of ras (shares DataBuffer with
     *            <tt>ras</tt>).
     */
    public static WritableRaster makeRasterWritable(Raster ras) {
        return makeRasterWritable(ras, ras.getMinX(), ras.getMinY());
    }

    /**
     * Coerces <tt>ras</tt> to be writable.  The returned Raster continues to
     * reference the DataBuffer from ras, so modifications to the returned
     * WritableRaster will be seen in ras.<p>
     *
     * You can specify a new location for the returned WritableRaster, this
     * is especially useful for constructing BufferedImages which require
     * the Raster to be at (0,0).
     *
     * This method should only be used if you need a WritableRaster due to
     * an interface (such as to construct a BufferedImage), but have no
     * intention of modifying the contents of the returned Raster.  If
     * you have any doubt about other users of the data in <tt>ras</tt>,
     * use copyRaster (above).
     *
     * @param ras The raster to make writable.
     *
     * @param minX The x location for the upper left corner of the
     *             returned WritableRaster.
     *
     * @param minY The y location for the upper left corner of the
     *             returned WritableRaster.
     *
     * @return A Writable version of <tT>ras</tt> with it's upper left
     *         hand coordinate set to minX, minY (shares it's DataBuffer
     *         with <tt>ras</tt>).
     */
    public static WritableRaster makeRasterWritable(Raster ras,
                                                    int minX, int minY) {
        WritableRaster ret = Raster.createWritableRaster(
            ras.getSampleModel(),
             ras.getDataBuffer(),
             new Point(0, 0));
        ret = ret.createWritableChild(
            ras.getMinX() - ras.getSampleModelTranslateX(),
             ras.getMinY() - ras.getSampleModelTranslateY(),
             ras.getWidth(), ras.getHeight(),
             minX, minY, null);
        return ret;
    }

    /**
     * Create a new ColorModel with it's alpha premultiplied state matching
     * newAlphaPreMult.
     * @param cm The ColorModel to change the alpha premult state of.
     * @param newAlphaPreMult The new state of alpha premult.
     * @return   A new colorModel that has isAlphaPremultiplied()
     *           equal to newAlphaPreMult.
     */
    public static ColorModel
        coerceColorModel(ColorModel cm, boolean newAlphaPreMult) {
        if (cm.isAlphaPremultiplied() == newAlphaPreMult) {
            return cm;
        }

        // Easiest way to build proper colormodel for new Alpha state...
        // Eventually this should switch on known ColorModel types and
        // only fall back on this hack when the CM type is unknown.
        WritableRaster wr = cm.createCompatibleWritableRaster(1, 1);
        return cm.coerceData(wr, newAlphaPreMult);
    }

    /**
     * Coerces data within a bufferedImage to match newAlphaPreMult,
     * Note that this can not change the colormodel of bi so you
     *
     * @param wr The raster to change the state of.
     * @param cm The colormodel currently associated with data in wr.
     * @param newAlphaPreMult The desired state of alpha Premult for raster.
     * @return A new colormodel that matches newAlphaPreMult.
     */
    public static ColorModel
        coerceData(WritableRaster wr, ColorModel cm, boolean newAlphaPreMult) {

        // System.out.println("CoerceData: " + cm.isAlphaPremultiplied() +
        //                    " Out: " + newAlphaPreMult);
        if (!cm.hasAlpha()) {
            // Nothing to do no alpha channel
            return cm;
        }

        if (cm.isAlphaPremultiplied() == newAlphaPreMult) {
            // nothing to do alpha state matches...
            return cm;
        }

        // System.out.println("CoerceData: " + wr.getSampleModel());

        if (newAlphaPreMult) {
            multiplyAlpha(wr);
        } else {
            divideAlpha(wr);
        }

        return coerceColorModel(cm, newAlphaPreMult);
    }

    public static void multiplyAlpha(WritableRaster wr) {
        if (is_BYTE_COMP_Data(wr.getSampleModel())) {
            mult_BYTE_COMP_Data(wr);
        } else if (is_INT_PACK_Data(wr.getSampleModel(), true)) {
            mult_INT_PACK_Data(wr);
        } else {
            int [] pixel = null;
            int    bands = wr.getNumBands();
            float  norm = 1f / 255f;
            int x0;
            int x1;
            int y0;
            int y1;
            int a;
            int b;
            float alpha;
            x0 = wr.getMinX();
            x1 = x0 + wr.getWidth();
            y0 = wr.getMinY();
            y1 = y0 + wr.getHeight();
            for (int y = y0; y < y1; y++) {
                for (int x = x0; x < x1; x++) {
                    pixel = wr.getPixel(x, y, pixel);
                    a = pixel[bands - 1];
                    if ((a >= 0) && (a < 255)) {
                        alpha = a * norm;
                        for (b = 0; b < bands - 1; b++) {
                            pixel[b] = (int)(pixel[b] * alpha + 0.5f);
                        }
                        wr.setPixel(x, y, pixel);
                    }
                }
            }
        }
    }

    public static void divideAlpha(WritableRaster wr) {
        if (is_BYTE_COMP_Data(wr.getSampleModel())) {
            divide_BYTE_COMP_Data(wr);
        } else if (is_INT_PACK_Data(wr.getSampleModel(), true)) {
            divide_INT_PACK_Data(wr);
        } else {
            int x0;
            int x1;
            int y0;
            int y1;
            int a;
            int b;
            float ialpha;
            int    bands = wr.getNumBands();
            int [] pixel = null;

            x0 = wr.getMinX();
            x1 = x0 + wr.getWidth();
            y0 = wr.getMinY();
            y1 = y0 + wr.getHeight();
            for (int y = y0; y < y1; y++) {
                for (int x = x0; x < x1; x++) {
                    pixel = wr.getPixel(x, y, pixel);
                    a = pixel[bands - 1];
                    if ((a > 0) && (a < 255)) {
                        ialpha = 255 / (float)a;
                        for (b = 0; b < bands - 1; b++) {
                            pixel[b] = (int)(pixel[b] * ialpha + 0.5f);
                        }
                        wr.setPixel(x, y, pixel);
                    }
                }
            }
        }
    }

    /**
     * Copies data from one bufferedImage to another paying attention
     * to the state of AlphaPreMultiplied.
     *
     * @param src The source
     * @param dst The destination
     */
    public static void
        copyData(BufferedImage src, BufferedImage dst) {
        Rectangle srcRect = new Rectangle(0, 0,
                                          src.getWidth(), src.getHeight());
        copyData(src, srcRect, dst, new Point(0, 0));
    }


    /**
     * Copies data from one bufferedImage to another paying attention
     * to the state of AlphaPreMultiplied.
     *
     * @param src The source
     * @param srcRect The Rectangle of source data to be copied
     * @param dst The destination
     * @param destP The Place for the upper left corner of srcRect in dst.
     */
    public static void
        copyData(BufferedImage src, Rectangle srcRect,
                 BufferedImage dst, Point destP) {

       /*
        if (srcCS != dstCS)
            throw new IllegalArgumentException
                ("Images must be in the same ColorSpace in order "+
                 "to copy Data between them");
        */
        boolean srcAlpha = src.getColorModel().hasAlpha();
        boolean dstAlpha = dst.getColorModel().hasAlpha();

        // System.out.println("Src has: " + srcAlpha +
        //                    " is: " + src.isAlphaPremultiplied());
        //
        // System.out.println("Dst has: " + dstAlpha +
        //                    " is: " + dst.isAlphaPremultiplied());

        if (srcAlpha == dstAlpha) {
            if (!srcAlpha
                    || src.isAlphaPremultiplied() == dst.isAlphaPremultiplied()) {
                // They match one another so just copy everything...
                copyData(src.getRaster(), dst.getRaster());
                return;
            }
        }

        // System.out.println("Using Slow CopyData");

        int [] pixel = null;
        Raster         srcR  = src.getRaster();
        WritableRaster dstR  = dst.getRaster();
        int            bands = dstR.getNumBands();

        int dx = destP.x - srcRect.x;
        int dy = destP.y - srcRect.y;

        int w  = srcRect.width;
        int x0 = srcRect.x;
        int y0 = srcRect.y;
        int y1 = y0 + srcRect.height - 1;

        if (!srcAlpha) {
            // Src has no alpha dest does so set alpha to 1.0 everywhere.
            // System.out.println("Add Alpha");
            int [] oPix = new int[bands * w];
            int out = (w * bands) - 1; // The 2 skips alpha channel
            while (out >= 0) {
                // Fill alpha channel with 255's
                oPix[out] = 255;
                out -= bands;
            }

            int b;
            int in;
            for (int y = y0; y <= y1; y++) {
                pixel = srcR.getPixels(x0, y, w, 1, pixel);
                in  = w * (bands - 1) - 1;
                out = (w * bands) - 2; // The 2 skips alpha channel on last pix
                switch (bands) {
                case 4:
                    while (in >= 0) {
                        oPix[out--] = pixel[in--];
                        oPix[out--] = pixel[in--];
                        oPix[out--] = pixel[in--];
                        out--;
                    }
                    break;
                default:
                    while (in >= 0) {
                        for (b = 0; b < bands - 1; b++) {
                            oPix[out--] = pixel[in--];
                        }
                        out--;
                    }
                }
                dstR.setPixels(x0 + dx, y + dy, w, 1, oPix);
            }
        } else if (dstAlpha && dst.isAlphaPremultiplied()) {
            // Src and dest have Alpha but we need to multiply it for dst.
            // System.out.println("Mult Case");
            int a;
            int b;
            int alpha;
            int in;
            int fpNorm = (1 << 24) / 255;
            int pt5 = 1 << 23;
            for (int y = y0; y <= y1; y++) {
                pixel = srcR.getPixels(x0, y, w, 1, pixel);
                in = bands * w - 1;
                switch (bands) {
                case 4:
                    while (in >= 0) {
                        a = pixel[in];
                        if (a == 255) {
                            in -= 4;
                        } else {
                            in--;
                            alpha = fpNorm * a;
                            pixel[in] = (pixel[in] * alpha + pt5) >>> 24;
                            in--;
                            pixel[in] = (pixel[in] * alpha + pt5) >>> 24;
                            in--;
                            pixel[in] = (pixel[in] * alpha + pt5) >>> 24;
                            in--;
                        }
                    }
                    break;
                default:
                    while (in >= 0) {
                        a = pixel[in];
                        if (a == 255) {
                            in -= bands;
                        } else {
                            in--;
                            alpha = fpNorm * a;
                            for (b = 0; b < bands - 1; b++) {
                                pixel[in] = (pixel[in] * alpha + pt5) >>> 24;
                                in--;
                            }
                        }
                    }
                }
                dstR.setPixels(x0 + dx, y + dy, w, 1, pixel);
            }
        } else if (dstAlpha && !dst.isAlphaPremultiplied()) {
            // Src and dest have Alpha but we need to divide it out for dst.
            // System.out.println("Div Case");
            int a;
            int b;
            int ialpha;
            int in;
            int fpNorm = 0x00FF0000;
            int pt5 = 1 << 15;
            for (int y = y0; y <= y1; y++) {
                pixel = srcR.getPixels(x0, y, w, 1, pixel);
                in = (bands * w) - 1;
                switch(bands) {
                case 4:
                    while (in >= 0) {
                        a = pixel[in];
                        if ((a <= 0) || (a >= 255)) {
                            in -= 4;
                        } else {
                            in--;
                            ialpha = fpNorm / a;
                            pixel[in] = (pixel[in] * ialpha + pt5) >>> 16;
                            in--;
                            pixel[in] = (pixel[in] * ialpha + pt5) >>> 16;
                            in--;
                            pixel[in] = (pixel[in] * ialpha + pt5) >>> 16;
                            in--;
                        }
                    }
                    break;
                default:
                    while (in >= 0) {
                        a = pixel[in];
                        if ((a <= 0) || (a >= 255)) {
                            in -= bands;
                        } else {
                            in--;
                            ialpha = fpNorm / a;
                            for (b = 0; b < bands - 1; b++) {
                                pixel[in] = (pixel[in] * ialpha + pt5) >>> 16;
                                in--;
                            }
                        }
                    }
                }
                dstR.setPixels(x0 + dx, y + dy, w, 1, pixel);
            }
        } else if (src.isAlphaPremultiplied()) {
            int [] oPix = new int[bands * w];
            // Src has alpha dest does not so unpremult and store...
            // System.out.println("Remove Alpha, Div Case");
            int a;
            int b;
            int ialpha;
            int in;
            int out;
            int fpNorm = 0x00FF0000;
            int pt5 = 1 << 15;
            for (int y = y0; y <= y1; y++) {
                pixel = srcR.getPixels(x0, y, w, 1, pixel);
                in  = (bands + 1) * w - 1;
                out = (bands * w) - 1;
                while (in >= 0) {
                    a = pixel[in];
                    in--;
                    if (a > 0) {
                        if (a < 255) {
                            ialpha = fpNorm / a;
                            for (b = 0; b < bands; b++) {
                                oPix[out--] = (pixel[in--] * ialpha + pt5) >>> 16;
                            }
                        } else {
                            for (b = 0; b < bands; b++) {
                                oPix[out--] = pixel[in--];
                            }
                        }
                    } else {
                        in -= bands;
                        for (b = 0; b < bands; b++) {
                            oPix[out--] = 255;
                        }
                    }
                }
                dstR.setPixels(x0 + dx, y + dy, w, 1, oPix);
            }
        } else {
            // Src has unpremult alpha, dest does not have alpha,
            // just copy the color channels over.
            Rectangle dstRect = new Rectangle(destP.x, destP.y,
                                              srcRect.width, srcRect.height);
            for (int b = 0; b < bands; b++) {
                copyBand(srcR, srcRect, b,
                         dstR, dstRect, b);
            }
        }
    }

    public static void copyBand(Raster         src, int srcBand,
                                WritableRaster dst, int dstBand) {

        Rectangle sR   = src.getBounds();
        Rectangle dR   = dst.getBounds();
        Rectangle cpR  = sR.intersection(dR);

        copyBand(src, cpR, srcBand, dst, cpR, dstBand);
    }

    public static void copyBand(Raster         src, Rectangle sR, int sBand,
                                WritableRaster dst, Rectangle dR, int dBand) {
        int dy = dR.y - sR.y;
        int dx = dR.x - sR.x;
        sR = sR.intersection(src.getBounds());
        dR = dR.intersection(dst.getBounds());
        int width;
        int height;
        if (dR.width  < sR.width) {
            width  = dR.width;
        } else {
            width  = sR.width;
        }
        if (dR.height < sR.height) {
            height = dR.height;
        } else {
            height = sR.height;
        }
        int x = sR.x + dx;
        int [] samples = null;
        for (int y = sR.y; y < sR.y + height; y++) {
            samples = src.getSamples(sR.x, y, width, 1, sBand, samples);
            dst.setSamples(x, y + dy, width, 1, dBand, samples);
        }
    }

    public static boolean is_INT_PACK_Data(SampleModel sm,
                                           boolean requireAlpha) {
        // Check ColorModel is of type DirectColorModel
        if (!(sm instanceof SinglePixelPackedSampleModel)) {
            return false;
        }

        // Check transfer type
        if (sm.getDataType() != DataBuffer.TYPE_INT) {
            return false;
        }

        SinglePixelPackedSampleModel sppsm;
        sppsm = (SinglePixelPackedSampleModel)sm;

        int [] masks = sppsm.getBitMasks();
        if (masks.length == 3) {
            if (requireAlpha) {
                return false;
            }
        } else if (masks.length != 4) {
            return false;
        }

        if (masks[0] != 0x00ff0000) {
            return false;
        }
        if (masks[1] != 0x0000ff00) {
            return false;
        }
        if (masks[2] != 0x000000ff) {
            return false;
        }
        if ((masks.length == 4)
            && (masks[3] != 0xff000000)) {
            return false;
        }

        return true;
    }

        public static boolean is_BYTE_COMP_Data(SampleModel sm) {
            // Check ColorModel is of type DirectColorModel
            if (!(sm instanceof ComponentSampleModel)) {
                return false;
            }

            // Check transfer type
            if (sm.getDataType() != DataBuffer.TYPE_BYTE) {
                return false;
            }

            return true;
        }

    protected static void divide_INT_PACK_Data(WritableRaster wr) {
        // System.out.println("Divide Int");

        SinglePixelPackedSampleModel sppsm;
        sppsm = (SinglePixelPackedSampleModel)wr.getSampleModel();

        final int width = wr.getWidth();

        final int scanStride = sppsm.getScanlineStride();
        DataBufferInt db = (DataBufferInt)wr.getDataBuffer();
        final int base
            = (db.getOffset()
               + sppsm.getOffset(wr.getMinX() - wr.getSampleModelTranslateX(),
                               wr.getMinY() - wr.getSampleModelTranslateY()));

        // Access the pixel data array
        final int[] pixels = db.getBankData()[0];
        for (int y = 0; y < wr.getHeight(); y++) {
            int sp = base + y * scanStride;
            final int end = sp + width;
            while (sp < end) {
                int pixel = pixels[sp];
                int a = pixel >>> 24;
                if (a <= 0) {
                    pixels[sp] = 0x00FFFFFF;
                } else if (a < 255) {
                    int aFP = (0x00FF0000 / a);
                    pixels[sp] =
                        ((a << 24)
                         | (((((pixel & 0xFF0000) >> 16) * aFP) & 0xFF0000))
                         | (((((pixel & 0x00FF00) >> 8) * aFP) & 0xFF0000) >> 8)
                         | (((((pixel & 0x0000FF))    * aFP) & 0xFF0000) >> 16));
                }
                sp++;
            }
        }
    }

    protected static void mult_INT_PACK_Data(WritableRaster wr) {
        // System.out.println("Multiply Int: " + wr);

        SinglePixelPackedSampleModel sppsm;
        sppsm = (SinglePixelPackedSampleModel)wr.getSampleModel();

        final int width = wr.getWidth();

        final int scanStride = sppsm.getScanlineStride();
        DataBufferInt db = (DataBufferInt)wr.getDataBuffer();
        final int base
            = (db.getOffset()
               + sppsm.getOffset(wr.getMinX() - wr.getSampleModelTranslateX(),
                               wr.getMinY() - wr.getSampleModelTranslateY()));
        // Access the pixel data array
        final int[] pixels = db.getBankData()[0];
        for (int y = 0; y < wr.getHeight(); y++) {
            int sp = base + y * scanStride;
            final int end = sp + width;
            while (sp < end) {
                int pixel = pixels[sp];
                int a = pixel >>> 24;
                if ((a >= 0) && (a < 255)) {   // this does NOT include a == 255 (0xff) !
                    pixels[sp] = ((a << 24)
                                  | ((((pixel & 0xFF0000) * a) >> 8) & 0xFF0000)
                                  | ((((pixel & 0x00FF00) * a) >> 8) & 0x00FF00)
                                  | ((((pixel & 0x0000FF) * a) >> 8) & 0x0000FF));
                }
                sp++;
            }
        }
    }


    protected static void divide_BYTE_COMP_Data(WritableRaster wr) {
        // System.out.println("Multiply Int: " + wr);

        ComponentSampleModel csm;
        csm = (ComponentSampleModel)wr.getSampleModel();

        final int width = wr.getWidth();

        final int scanStride = csm.getScanlineStride();
        final int pixStride  = csm.getPixelStride();
        final int [] bandOff = csm.getBandOffsets();

        DataBufferByte db = (DataBufferByte)wr.getDataBuffer();
        final int base
            = (db.getOffset()
               + csm.getOffset(wr.getMinX() - wr.getSampleModelTranslateX(),
                             wr.getMinY() - wr.getSampleModelTranslateY()));

        int aOff = bandOff[bandOff.length - 1];
        int bands = bandOff.length - 1;

        // Access the pixel data array
        final byte[] pixels = db.getBankData()[0];
        for (int y = 0; y < wr.getHeight(); y++) {
            int sp = base + y * scanStride;
            final int end = sp + width * pixStride;
            while (sp < end) {
              int a = pixels[sp + aOff] & 0xFF;
              if (a == 0) {
                for (int b = 0; b < bands; b++) {
                  pixels[sp + bandOff[b]] = (byte)0xFF;
                }
              } else if (a < 255) {         // this does NOT include a == 255 (0xff) !
                int aFP = (0x00FF0000 / a);
                for (int b = 0; b < bands; b++) {
                  int i = sp + bandOff[b];
                  pixels[i] = (byte)(((pixels[i] & 0xFF) * aFP) >>> 16);
                }
              }
              sp += pixStride;
            }
        }
    }

    protected static void mult_BYTE_COMP_Data(WritableRaster wr) {
        // System.out.println("Multiply Int: " + wr);

        ComponentSampleModel csm;
        csm = (ComponentSampleModel)wr.getSampleModel();

        final int width = wr.getWidth();

        final int scanStride = csm.getScanlineStride();
        final int pixStride  = csm.getPixelStride();
        final int [] bandOff = csm.getBandOffsets();

        DataBufferByte db = (DataBufferByte)wr.getDataBuffer();
        final int base
            = (db.getOffset()
               + csm.getOffset(wr.getMinX() - wr.getSampleModelTranslateX(),
                             wr.getMinY() - wr.getSampleModelTranslateY()));


        int aOff = bandOff[bandOff.length - 1];
        int bands = bandOff.length - 1;

        // Access the pixel data array
        final byte[] pixels = db.getBankData()[0];
        for (int y = 0; y < wr.getHeight(); y++) {
            int sp = base + y * scanStride;
            final int end = sp + width * pixStride;
            while (sp < end) {
              int a = pixels[sp + aOff] & 0xFF;
              if (a != 0xFF) {
                for (int b = 0; b < bands; b++) {
                  int i = sp + bandOff[b];
                  pixels[i] = (byte)(((pixels[i] & 0xFF) * a) >> 8);
                }
              }
              sp += pixStride;
            }
        }
    }

/*
  This is skanky debugging code that might be useful in the future:

            if (count == 33) {
                String label = "sub [" + x + ", " + y + "]: ";
                org.ImageDisplay.showImage
                    (label, subBI);
                org.ImageDisplay.printImage
                    (label, subBI,
                     new Rectangle(75-iR.x, 90-iR.y, 32, 32));

            }


            // if ((count++ % 50) == 10)
            //     org.ImageDisplay.showImage("foo: ", subBI);


            Graphics2D realG2D = g2d;
            while (realG2D instanceof sun.java2d.ProxyGraphics2D) {
                realG2D = ((sun.java2d.ProxyGraphics2D)realG2D).getDelegate();
            }
            if (realG2D instanceof sun.awt.image.BufferedImageGraphics2D) {
                count++;
                if (count == 34) {
                    RenderedImage ri;
                    ri = ((sun.awt.image.BufferedImageGraphics2D)realG2D).bufImg;
                    // g2d.setComposite(SVGComposite.OVER);
                    // org.ImageDisplay.showImage("Bar: " + count, cr);
                    org.ImageDisplay.printImage("Bar: " + count, cr,
                                                new Rectangle(75, 90, 32, 32));

                    org.ImageDisplay.showImage ("Foo: " + count, ri);
                    org.ImageDisplay.printImage("Foo: " + count, ri,
                                                new Rectangle(75, 90, 32, 32));

                    System.out.println("BI: "   + ri);
                    System.out.println("BISM: " + ri.getSampleModel());
                    System.out.println("BICM: " + ri.getColorModel());
                    System.out.println("BICM class: " + ri.getColorModel().getClass());
                    System.out.println("BICS: " + ri.getColorModel().getColorSpace());
                    System.out.println
                        ("sRGB CS: " +
                         ColorSpace.getInstance(ColorSpace.CS_sRGB));
                    System.out.println("G2D info");
                    System.out.println("\tComposite: " + g2d.getComposite());
                    System.out.println("\tTransform" + g2d.getTransform());
                    java.awt.RenderingHints rh = g2d.getRenderingHints();
                    java.util.Set keys = rh.keySet();
                    java.util.Iterator iter = keys.iterator();
                    while (iter.hasNext()) {
                        Object o = iter.next();

                        System.out.println("\t" + o.toString() + " -> " +
                                           rh.get(o).toString());
                    }

                    ri = cr;
                    System.out.println("RI: "   + ri);
                    System.out.println("RISM: " + ri.getSampleModel());
                    System.out.println("RICM: " + ri.getColorModel());
                    System.out.println("RICM class: " + ri.getColorModel().getClass());
                    System.out.println("RICS: " + ri.getColorModel().getColorSpace());
                }
            }
*/

    /**
     * Extracts an alpha raster from a RenderedImage. The method tries to avoid copying data
     * unnecessarily by checking if the RenderedImage is a BufferedImage which offers suitable
     * direct methods.
     * @param image the image
     * @return the alpha raster
     */
    public static Raster getAlphaRaster(RenderedImage image) {
        ColorModel cm = image.getColorModel();
        if (!cm.hasAlpha() || cm.getTransparency() != ColorModel.TRANSLUCENT) {
            throw new IllegalStateException("Image doesn't have an alpha channel");
        }
        Raster alpha;
        if (image instanceof BufferedImage) {
            //Optimization possible with BufferedImage (No copying)
            alpha = ((BufferedImage)image).getAlphaRaster();
        } else {
            WritableRaster wraster = GraphicsUtil.makeRasterWritable(image.getData());
            alpha = image.getColorModel().getAlphaRaster(wraster);
        }
        return alpha;
    }


}
