/*
 *  Copyright (C) 2010-2021 JPEXS, All rights reserved.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3.0 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.
 */
package com.jpexs.decompiler.flash.types.filters;

import com.jpexs.decompiler.flash.types.RGBA;
import com.jpexs.helpers.SerializableImage;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.LinearGradientPaint;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp;
import java.awt.image.DataBufferInt;
import java.awt.image.Kernel;

/**
 *
 * @author JPEXS
 */
public class Filtering {

    public static final int INNER = 1;

    public static final int OUTER = 2;

    public static final int FULL = 3;

    private static final Color ALPHA = new Color(0, 0, 0, 0);

    private static final Point POINT_0_0 = new Point(0, 0);

    private static final Point POINT_255_0 = new Point(255, 0);

    private static final Point POINT_511_0 = new Point(511, 0);

    private static final Rectangle RECTANGLE_256_1 = new Rectangle(256, 1);

    private static final Rectangle RECTANGLE_512_1 = new Rectangle(512, 1);

    private static void boxBlurHorizontal(int[] pixels, int[] mask, int[] newColors, int w, int h, int radius) {
        int index = 0;

        for (int y = 0; y < h; y++) {
            int hits = 0;
            long r = 0;
            long g = 0;
            long b = 0;
            long a = 0;
            for (int x = -radius; x < w; x++) {
                int oldPixel = x - radius - 1;
                if (oldPixel >= 0) {

                    int color = pixels[index + oldPixel];
                    if ((mask == null) || (((mask[index + oldPixel] >> 24) & 0xff) > 0)) {
                        if (color != 0) {
                            a -= (color >> 24) & 0xff;
                            r -= ((color >> 16) & 0xff);
                            g -= ((color >> 8) & 0xff);
                            b -= ((color) & 0xff);

                        }
                        hits--;
                    }
                }

                int newPixel = x + radius;
                if (newPixel < w) {
                    int color = pixels[index + newPixel];
                    if ((mask == null) || (((mask[index + newPixel] >> 24) & 0xff) > 0)) {
                        if (color != 0) {
                            a += (color >> 24) & 0xff;
                            r += ((color >> 16) & 0xff);
                            g += ((color >> 8) & 0xff);
                            b += ((color) & 0xff);
                        }
                        hits++;
                    }
                }

                if (x >= 0) {
                    if ((mask == null) || (((mask[index + x] >> 24) & 0xff) > 0)) {
                        if (hits == 0) {
                            newColors[x] = 0;
                        } else {
                            newColors[x] = RGBA.toInt((int) (r / hits) & 0xff, (int) (g / hits) & 0xff, (int) (b / hits) & 0xff, (int) (a / hits));
                        }
                    } else {
                        newColors[x] = 0;
                    }
                }
            }

            System.arraycopy(newColors, 0, pixels, index, w);

            index += w;
        }
    }

    private static void boxBlurVertical(int[] pixels, int[] mask, int[] newColors, int w, int h, int radius) {
        int oldPixelOffset = -(radius + 1) * w;
        int newPixelOffset = (radius) * w;

        for (int x = 0; x < w; x++) {
            int hits = 0;
            long r = 0;
            long g = 0;
            long b = 0;
            long a = 0;
            int index = -radius * w + x;
            for (int y = -radius; y < h; y++) {
                int oldPixel = y - radius - 1;
                if (oldPixel >= 0) {
                    int color = pixels[index + oldPixelOffset];
                    if ((mask == null) || (((mask[index + oldPixelOffset] >> 24) & 0xff) > 0)) {
                        if (color != 0) {
                            a -= (color >> 24) & 0xff;
                            r -= ((color >> 16) & 0xff);
                            g -= ((color >> 8) & 0xff);
                            b -= ((color) & 0xff);

                        }
                        hits--;
                    }

                }

                int newPixel = y + radius;
                if (newPixel < h) {
                    if ((mask == null) || (((mask[index + newPixelOffset] >> 24) & 0xff) > 0)) {
                        int color = pixels[index + newPixelOffset];
                        if (color != 0) {
                            a += (color >> 24) & 0xff;
                            r += ((color >> 16) & 0xff);
                            g += ((color >> 8) & 0xff);
                            b += ((color) & 0xff);

                        }
                        hits++;
                    }
                }

                if (y >= 0) {
                    if ((mask == null) || (((mask[y * w + x] >> 24) & 0xff) > 0)) {
                        if (hits == 0) {
                            newColors[y] = 0;
                        } else {
                            newColors[y] = RGBA.toInt((int) (r / hits) & 0xff, (int) (g / hits) & 0xff, (int) (b / hits) & 0xff, (int) (a / hits) & 0xff);
                        }
                    } else {
                        newColors[y] = 0;
                    }
                }

                index += w;
            }

            for (int y = 0; y < h; y++) {
                pixels[y * w + x] = newColors[y];
            }
        }
    }

    private static void premultiply(int[] p) {
        int length = p.length;
        int offset = 0;
        length += offset;
        for (int i = offset; i < length; i++) {
            int rgb = p[i];
            int a = rgb >> 24 & 0xff;
            int r = rgb >> 16 & 0xff;
            int g = rgb >> 8 & 0xff;
            int b = rgb & 0xff;
            float f = (float) a * 0.003921569F;
            r = (int) ((float) r * f);
            g = (int) ((float) g * f);
            b = (int) ((float) b * f);
            p[i] = a << 24 | r << 16 | g << 8 | b;
        }
    }

    private static void unpremultiply(int[] p) {
        int length = p.length;
        int offset = 0;
        length += offset;
        for (int i = offset; i < length; i++) {
            int rgb = p[i];
            int a = rgb >> 24 & 0xff;
            int r = rgb >> 16 & 0xff;
            int g = rgb >> 8 & 0xff;
            int b = rgb & 0xff;
            if (a == 0 || a == 255) {
                continue;
            }
            float f = 255F / (float) a;
            r = (int) ((float) r * f);
            g = (int) ((float) g * f);
            b = (int) ((float) b * f);
            if (r > 255) {
                r = 255;
            }
            if (g > 255) {
                g = 255;
            }
            if (b > 255) {
                b = 255;
            }
            p[i] = a << 24 | r << 16 | g << 8 | b;
        }
    }

    public static SerializableImage blur(SerializableImage src, int hRadius, int vRadius, int iterations) {
        int[] pixels = (int[]) getRGB(src.getBufferedImage()).clone();
        int width = src.getWidth();
        int height = src.getHeight();
        blur(pixels, width, height, hRadius, vRadius, iterations, null);
        BufferedImage ret = new BufferedImage(width, height, src.getType());
        setRGB(ret, width, height, pixels);
        return new SerializableImage(ret);
    }

    private static void blur(int[] src, int width, int height, int hRadius, int vRadius, int iterations, int[] mask) {
        int[] inPixels = src;
        premultiply(inPixels);

        int[] tempRow = new int[width];
        int[] tempColumn = new int[height];
        for (int i = 0; i < iterations; i++) {
            boxBlurHorizontal(inPixels, mask, tempRow, width, height, hRadius / 2);
            boxBlurVertical(inPixels, mask, tempColumn, width, height, vRadius / 2);
        }
        unpremultiply(inPixels);
    }

    public static SerializableImage bevel(SerializableImage src, int blurX, int blurY, float strength, int type, int highlightColor, int shadowColor, float angle, float distance, boolean knockout, int iterations) {
        return new SerializableImage(gradientBevel(src.getBufferedImage(), new Color[]{
            new Color(shadowColor, true),
            new Color(shadowColor & 0x00ffffff, true),
            new Color(highlightColor & 0x00ffffff, true),
            new Color(highlightColor, true)
        }, new float[]{0, 127f / 255f, 128f / 255f, 1}, blurX, blurY, strength, type, angle, distance, knockout, iterations));
    }

    public static SerializableImage gradientBevel(SerializableImage src, Color[] colors, float[] ratios, int blurX, int blurY, float strength, int type, float angle, float distance, boolean knockout, int iterations) {
        return new SerializableImage(gradientBevel(src.getBufferedImage(), colors, ratios, blurX, blurY, strength, type, angle, distance, knockout, iterations));
    }

    private static BufferedImage gradientBevel(BufferedImage src, Color[] colors, float[] ratios, int blurX, int blurY, float strength, int type, float angle, float distance, boolean knockout, int iterations) {
        int width = src.getWidth();
        int height = src.getHeight();
        BufferedImage retImg = new BufferedImage(width, height, src.getType());
        int[] srcPixels = getRGB(src);

        int[] revPixels = new int[srcPixels.length];
        for (int i = 0; i < srcPixels.length; i++) {
            revPixels[i] = (srcPixels[i] & 0xffffff) + ((255 - ((srcPixels[i] >> 24) & 0xff)) << 24);
        }

        BufferedImage gradient = new BufferedImage(512, 1, src.getType());
        Graphics2D gg = gradient.createGraphics();

        Point p1 = POINT_0_0;
        Point p2 = POINT_511_0;
        gg.setPaint(new LinearGradientPaint(p1, p2, ratios, colors));
        gg.fill(RECTANGLE_512_1);
        int[] gradientPixels = getRGB(gradient);

        BufferedImage shadowInner = null;
        BufferedImage hilightInner = null;
        if (type != OUTER) {
            BufferedImage hilightIm = dropShadow(src, 0, 0, angle, distance, Color.red, true, iterations, strength, true);//new DropShadowFilter(blurX, blurY, strength, inner ? highlightColor : shadowColor, angle, distance, inner, true, iterations).filter(src
            BufferedImage shadowIm = dropShadow(src, 0, 0, angle + 180, distance, Color.blue, true, iterations, strength, true); //new DropShadowFilter(blurX, blurY, strength, inner ? shadowColor : highlightColor, angle + 180, distance, inner, true, iterations).filter(src);
            BufferedImage h2 = new BufferedImage(width, height, src.getType());
            BufferedImage s2 = new BufferedImage(width, height, src.getType());
            Graphics2D hc = h2.createGraphics();
            Graphics2D sc = s2.createGraphics();
            hc.drawImage(hilightIm, 0, 0, null);
            hc.setComposite(AlphaComposite.DstOut);
            hc.drawImage(shadowIm, 0, 0, null);

            sc.drawImage(shadowIm, 0, 0, null);
            sc.setComposite(AlphaComposite.DstOut);
            sc.drawImage(hilightIm, 0, 0, null);
            shadowInner = s2;
            hilightInner = h2;
        }

        BufferedImage shadowOuter = null;
        BufferedImage hilightOuter = null;
        if (type != INNER) {
            BufferedImage hilightIm = dropShadow(src, 0, 0, angle + 180, distance, Color.red, false, iterations, strength, true);//new DropShadowFilter(blurX, blurY, strength, inner ? highlightColor : shadowColor, angle, distance, inner, true, iterations).filter(src
            BufferedImage shadowIm = dropShadow(src, 0, 0, angle, distance, Color.blue, false, iterations, strength, true); //new DropShadowFilter(blurX, blurY, strength, inner ? shadowColor : highlightColor, angle + 180, distance, inner, true, iterations).filter(src);
            BufferedImage h2 = new BufferedImage(width, height, src.getType());
            BufferedImage s2 = new BufferedImage(width, height, src.getType());
            Graphics2D hc = h2.createGraphics();
            Graphics2D sc = s2.createGraphics();
            hc.drawImage(hilightIm, 0, 0, null);
            hc.setComposite(AlphaComposite.DstOut);
            hc.drawImage(shadowIm, 0, 0, null);

            sc.drawImage(shadowIm, 0, 0, null);
            sc.setComposite(AlphaComposite.DstOut);
            sc.drawImage(hilightIm, 0, 0, null);
            shadowOuter = s2;
            hilightOuter = h2;
        }

        BufferedImage hilightIm = null;
        BufferedImage shadowIm = null;
        switch (type) {
            case OUTER:
                hilightIm = hilightOuter;
                shadowIm = shadowOuter;
                break;
            case INNER:
                hilightIm = hilightInner;
                shadowIm = shadowInner;
                break;
            case FULL:
                hilightIm = hilightInner;
                shadowIm = shadowInner;
                Graphics2D hc = hilightIm.createGraphics();
                hc.setComposite(AlphaComposite.SrcOver);
                hc.drawImage(hilightOuter, 0, 0, null);
                Graphics2D sc = shadowIm.createGraphics();
                sc.setComposite(AlphaComposite.SrcOver);
                sc.drawImage(shadowOuter, 0, 0, null);
                break;
        }

        int[] mask = null;
        if (type == INNER) {
            mask = srcPixels;
        }
        if (type == OUTER) {
            mask = revPixels;
        }

        Graphics2D retc = retImg.createGraphics();
        retc.setColor(Color.black);
        retc.fillRect(0, 0, width, height);
        retc.setComposite(AlphaComposite.SrcOver);
        retc.drawImage(shadowIm, 0, 0, null);
        retc.drawImage(hilightIm, 0, 0, null);

        int[] ret = getRGB(retImg);
        blur(ret, width, height, blurX, blurY, iterations, mask);

        for (int i = 0; i < srcPixels.length; i++) {
            int ah = (int) (((ret[i] >> 16) & 0xFF) * strength);
            int as = (int) ((ret[i] & 0xFF) * strength);
            int ra = cut(ah - as, -255, 255);
            ret[i] = gradientPixels[255 + ra];
        }

        setRGB(retImg, width, height, ret);

        if (!knockout) {
            Graphics2D g = retImg.createGraphics();
            g.setComposite(AlphaComposite.DstOver);
            g.drawImage(src, 0, 0, null);
        }
        return retImg;
    }

    public static SerializableImage glow(SerializableImage src, int blurX, int blurY, float strength, Color color, boolean inner, boolean knockout, int iterations) {
        return new SerializableImage(dropShadow(src.getBufferedImage(), blurX, blurY, 45, 0, color, inner, iterations, strength, knockout));
    }

    public static SerializableImage dropShadow(SerializableImage src, int blurX, int blurY, float angle, double distance, Color color, boolean inner, int iterations, float strength, boolean knockout) {
        return new SerializableImage(dropShadow(src.getBufferedImage(), blurX, blurY, angle, distance, color, inner, iterations, strength, knockout));
    }

    private static int cut(int val, int min, int max) {
        if (val > max) {
            val = max;
        }
        if (val < min) {
            val = min;
        }
        return val;
    }

    private static BufferedImage dropShadow(BufferedImage src, int blurX, int blurY, float angle, double distance, Color color, boolean inner, int iterations, float strength, boolean knockout) {
        int width = src.getWidth();
        int height = src.getHeight();
        int[] srcPixels = getRGB(src);
        int[] shadow = new int[srcPixels.length];
        for (int i = 0; i < srcPixels.length; i++) {
            int alpha = (srcPixels[i] >> 24) & 0xff;
            if (inner) {
                alpha = 255 - alpha;
            }

            shadow[i] = RGBA.toInt(color.getRed(), color.getGreen(), color.getBlue(), cut(color.getAlpha() * alpha / 255 * strength));
        }

        Color colorFirst = Color.BLACK;
        Color colorAlpha = ALPHA;
        double angleRad = angle / 180 * Math.PI;
        double moveX = (distance * Math.cos(angleRad));
        double moveY = (distance * Math.sin(angleRad));
        shadow = moveRGB(width, height, shadow, moveX, moveY, inner ? colorFirst : colorAlpha);

        if (blurX > 0 || blurY > 0) {
            blur(shadow, width, height, blurX, blurY, iterations, null);
        }

        for (int i = 0; i < shadow.length; i++) {
            int mask = (srcPixels[i] >> 24) & 0xff;
            if (!inner) {
                mask = 255 - mask;
            }
            shadow[i] = shadow[i] & 0xffffff + ((mask * ((shadow[i] >> 24) & 0xff) / 255) << 24);
        }

        BufferedImage retCanvas = new BufferedImage(width, height, src.getType());
        setRGB(retCanvas, width, height, shadow);

        if (!knockout) {
            Graphics2D g = retCanvas.createGraphics();
            g.setComposite(AlphaComposite.DstOver);
            g.drawImage(src, 0, 0, null);
        }

        return retCanvas;
    }

    public static SerializableImage gradientGlow(SerializableImage src, int blurX, int blurY, float angle, double distance, Color[] colors, float[] ratios, int type, int iterations, float strength, boolean knockout) {
        return new SerializableImage(gradientGlow(src.getBufferedImage(), blurX, blurY, angle, distance, colors, ratios, type, iterations, strength, knockout));
    }

    private static BufferedImage gradientGlow(BufferedImage src, int blurX, int blurY, float angle, double distance, Color[] colors, float[] ratios, int type, int iterations, float strength, boolean knockout) {

        int width = src.getWidth();
        int height = src.getHeight();
        BufferedImage gradCanvas = new BufferedImage(256, 1, src.getType());
        Graphics2D gg = gradCanvas.createGraphics();

        Point p1 = POINT_0_0;
        Point p2 = POINT_255_0;
        gg.setPaint(new LinearGradientPaint(p1, p2, ratios, colors));
        gg.fill(RECTANGLE_256_1);
        int[] gradientPixels = getRGB(gradCanvas);

        double angleRad = angle / 180 * Math.PI;
        double moveX = (distance * Math.cos(angleRad));
        double moveY = (distance * Math.sin(angleRad));
        int[] srcPixels = getRGB(src);
        int[] revPixels = new int[srcPixels.length];
        for (int i = 0; i < srcPixels.length; i++) {
            revPixels[i] = (srcPixels[i] & 0xffffff) + ((255 - ((srcPixels[i] >> 24) & 0xff)) << 24);
        }

        int[] shadow = new int[srcPixels.length];
        for (int i = 0; i < srcPixels.length; i++) {
            shadow[i] = 0 + ((cut(strength * ((srcPixels[i] >> 24) & 0xff))) << 24);
        }

        Color colorAlpha = ALPHA;
        shadow = moveRGB(width, height, shadow, moveX, moveY, colorAlpha);

        int[] mask = null;
        if (type == INNER) {
            mask = srcPixels;
        }
        if (type == OUTER) {
            mask = revPixels;
        }

        blur(shadow, width, height, blurX, blurY, iterations, mask);

        if (mask != null) {
            for (int i = 0; i < mask.length; i++) {
                int m = (mask[i] >> 24);
                if (m == 0) {
                    shadow[i] = 0;
                }
            }
        }

        for (int i = 0; i < shadow.length; i++) {
            int a = (shadow[i] >> 24) & 0xff;
            shadow[i] = gradientPixels[a];
        }

        BufferedImage retCanvas = new BufferedImage(width, height, src.getType());
        setRGB(retCanvas, width, height, shadow);

        if (!knockout) {
            Graphics2D retImg = retCanvas.createGraphics();
            retImg.setComposite(AlphaComposite.DstOver);
            retImg.drawImage(src, 0, 0, null);
        }

        return retCanvas;
    }

    private static int[] getRGB(BufferedImage image) {
        int type = image.getType();
        if (type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB) {
            return ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
        }
        int width = image.getWidth();
        return image.getRGB(0, 0, width, image.getHeight(), null, 0, width);
    }

    public static void setRGB(BufferedImage image, int width, int height, int[] pixels) {
        int type = image.getType();
        if (type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB) {
            image.getRaster().setDataElements(0, 0, width, height, pixels);
        } else {
            image.setRGB(0, 0, width, height, pixels, 0, width);
        }
    }

    private static int[] moveRGB(int width, int height, int[] rgb, double deltaX, double deltaY, Color fill) {
        BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        setRGB(img, width, height, rgb);
        BufferedImage retImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = (Graphics2D) retImg.getGraphics();
        g.setPaint(fill);
        g.fillRect(0, 0, width, height);
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        g.setTransform(AffineTransform.getTranslateInstance(deltaX, deltaY));
        g.setComposite(AlphaComposite.Src);
        g.drawImage(img, 0, 0, null);
        return getRGB(retImg);
    }

    public static SerializableImage convolution(SerializableImage src, float[] matrix, int w, int h) {
        BufferedImage dst = new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
        BufferedImageOp op = new ConvolveOp(new Kernel(w, h, matrix), ConvolveOp.EDGE_ZERO_FILL, new RenderingHints(null));
        op.filter(src.getBufferedImage(), dst);
        return new SerializableImage(dst);
    }

    public static SerializableImage colorMatrix(SerializableImage src, float[][] matrix) {
        /*BandCombineOp changeColors = new BandCombineOp(matrix, new RenderingHints(null));
         Raster sourceRaster = src.getRaster();
         WritableRaster displayRaster = sourceRaster.createCompatibleWritableRaster();
         changeColors.filter(sourceRaster, displayRaster);
         return new SerializableImage(src.getColorModel(), displayRaster, true, null);*/
        BufferedImage dst = new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
        int[] pixels = getRGB(src.getBufferedImage()).clone();
        for (int i = 0; i < pixels.length; i++) {
            int rgb = pixels[i];
            int a = (rgb >> 24) & 0xff;
            int r = (rgb >> 16) & 0xff;
            int g = (rgb >> 8) & 0xff;
            int b = rgb & 0xff;
            float[] mr = matrix[0];
            int r2 = cut(mr[0] * r + mr[1] * g + mr[2] * b + mr[3] * a + mr[4]);
            float[] mg = matrix[1];
            int g2 = cut(mg[0] * r + mg[1] * g + mg[2] * b + mg[3] * a + mg[4]);
            float[] mb = matrix[2];
            int b2 = cut(mb[0] * r + mb[1] * g + mb[2] * b + mb[3] * a + mb[4]);
            float[] ma = matrix[3];
            int a2 = cut(ma[0] * r + ma[1] * g + ma[2] * b + ma[3] * a + ma[4]);
            pixels[i] = (a2 << 24) | (r2 << 16) | (g2 << 8) | b2;
        }
        setRGB(dst, src.getWidth(), src.getHeight(), pixels);
        return new SerializableImage(dst);
    }

    private static int cut(double val) {
        int i = (int) val;
        if (i < 0) {
            i = 0;
        }
        if (i > 255) {
            i = 255;
        }
        return i;
    }

    public static int colorEffect(int rgb,
            int redAddTerm, int greenAddTerm, int blueAddTerm, int alphaAddTerm,
            int redMultTerm, int greenMultTerm, int blueMultTerm, int alphaMultTerm) {
        int a = (rgb >> 24) & 0xff;
        int r = (rgb >> 16) & 0xff;
        int g = (rgb >> 8) & 0xff;
        int b = rgb & 0xff;
        r = cut(((r * redMultTerm) / 256) + redAddTerm);
        g = cut(((g * greenMultTerm) / 256) + greenAddTerm);
        b = cut(((b * blueMultTerm) / 256) + blueAddTerm);
        a = cut(((a * alphaMultTerm) / 256) + alphaAddTerm);
        return (a << 24) | (r << 16) | (g << 8) | b;
    }

    public static SerializableImage colorEffect(SerializableImage src,
            int redAddTerm, int greenAddTerm, int blueAddTerm, int alphaAddTerm,
            int redMultTerm, int greenMultTerm, int blueMultTerm, int alphaMultTerm) {
        BufferedImage dst = new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
        int[] pixels = getRGB(src.getBufferedImage()).clone();
        for (int i = 0; i < pixels.length; i++) {
            int rgb = pixels[i];
            int a = (rgb >> 24) & 0xff;
            int r = (rgb >> 16) & 0xff;
            int g = (rgb >> 8) & 0xff;
            int b = rgb & 0xff;
            r = cut(((r * redMultTerm) / 256) + redAddTerm);
            g = cut(((g * greenMultTerm) / 256) + greenAddTerm);
            b = cut(((b * blueMultTerm) / 256) + blueAddTerm);
            a = cut(((a * alphaMultTerm) / 256) + alphaAddTerm);
            pixels[i] = (a << 24) | (r << 16) | (g << 8) | b;
        }
        setRGB(dst, src.getWidth(), src.getHeight(), pixels);
        return new SerializableImage(dst);
    }
}
