/*
XPAGES pour WebJaxe

Copyright (C) 2007 Observatoire de Paris-Meudon

Ce programme est un logiciel libre ; vous pouvez le redistribuer et/ou le modifier conformément aux dispositions de la Licence Publique Générale GNU, telle que publiée par la Free Software Foundation ; version 2 de la licence, ou encore (à votre choix) toute version ultérieure.

Ce programme est distribué dans l'espoir qu'il sera utile, mais SANS AUCUNE GARANTIE ; sans même la garantie implicite de COMMERCIALISATION ou D'ADAPTATION A UN OBJET PARTICULIER. Pour plus de détail, voir la Licence Publique Générale GNU .

Vous devez avoir reçu un exemplaire de la Licence Publique Générale GNU en même temps que ce programme ; si ce n'est pas le cas, écrivez à la Free Software Foundation Inc., 675 Mass Ave, Cambridge, MA 02139, Etats-Unis.
*/

package xpages;

import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.util.Hashtable;

import xpages.gif.GifDecoder;
import xpages.fmsware.AnimatedGifEncoder;

public class GIFAnim implements ImAnim {
    static final boolean optimisation = true;
    Image img1 = null;
    GifDecoder deconeur;
    private CartePalette carteCouleurs;
    
    // debugging
    public static void main(String[] args) {
        if (args.length != 4) {
            System.err.println("il faut 4 arguments");
            return;
        }
        File forig = new File(args[0]);
        if (!forig.exists()) {
            System.err.println("le fichier n'existe pas");
            return;
        }
        File fdest = new File(args[1]);
        int largeur;
        int hauteur;
        try {
            largeur = (new Integer(args[2])).intValue();
            hauteur = (new Integer(args[3])).intValue();
        } catch (Exception ex) {
            ex.printStackTrace();
            return;
        }
        GIFAnim anim = new GIFAnim();
        try {
            anim.open(forig);
            anim.rescale(fdest, largeur, hauteur);
        } catch (IOException ex) {
            System.err.println("IOException : " + ex.getMessage());
            return;
        }
    }
    
    public void open(File f) throws IOException {
        deconeur = new GifDecoder();
        deconeur.setIndexation(true);
        int err = deconeur.read(f.getPath());
        if (err != GifDecoder.STATUS_OK) {
            if (err == GifDecoder.STATUS_FORMAT_ERROR)
                System.err.println("GifDecoder: format error");
            else if (err == GifDecoder.STATUS_OPEN_ERROR)
                System.err.println("GifDecoder: open error");
        }
        if (deconeur.getFrameCount() > 0)
            img1 = deconeur.getFrame(0);
        carteCouleurs = null;
    }
    
    public int getWidth() {
        if (img1 == null)
            return(-1);
        else
            return(img1.getWidth(null));
    }
    
    public int getHeight() {
        if (img1 == null)
            return(-1);
        else
            return(img1.getHeight(null));
    }
    
    public int getImageCount() {
        return(deconeur.getFrameCount());
    }
    
    public Image getImage(int n) {
       if (n > getImageCount())
           return(null);
        BufferedImage offimg = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
        Graphics2D offg = offimg.createGraphics();
        for (int i=0; i<n; i++) {
            Image img = deconeur.getFrame(i);
            if (img == null)
                break;
            offg.drawImage(img, 0, 0, null);
        }
        offg.dispose();
        return(offimg);
    }
    
    public void rescale(File newf, int width, int height) throws IOException {
        AnimatedGifEncoder e = new AnimatedGifEncoder();
        e.start(newf.getPath());
        int loopCount = deconeur.getLoopCount();
        if (loopCount >= 0)
            e.setRepeat(loopCount);
        
        int oldwidth = getWidth();
        int oldheight = getHeight();
        
        int backrgb = deconeur.getBgColor();
        //System.out.println("bgcolor: " + Long.toString(backrgb & 0xFFFFFFFFL, 16).toUpperCase());
        
        int[] gct = deconeur.getGct();
        int transIndex ;
        if (deconeur.getTransparency(0))
            transIndex = deconeur.getTransIndex();
        else
            transIndex = -1;
        IndexColorModel gicm = null;
        if (gct != null && deconeur.getFrameCount() > 1) {
            //System.out.println("palette globale");
            byte[] r = new byte[gct.length];
            byte[] g = new byte[gct.length];
            byte[] b = new byte[gct.length];
            for (int i=0; i<gct.length; i++) {
                r[i] = (byte)((gct[i] >> 16) & 0x00FF);
                g[i] = (byte)((gct[i] >> 8) & 0x0000FF);
                b[i] = (byte)(gct[i] & 0x000000FF);
            }
            if (transIndex != -1)
                gicm = new IndexColorModel(8, gct.length, r, g, b, transIndex);
            else {
                gicm = new IndexColorModel(8, gct.length, r, g, b);
                if (optimisation)
                    gicm = icmTrans(gicm);
            }
            byte[] intergct = new byte[3*gct.length];
            for (int i=0; i<gct.length; i++) {
                intergct[i*3] = r[i];
                intergct[i*3+1] = g[i];
                intergct[i*3+2] = b[i];
            }
            e.setGct(intergct);
        }
        
        // image sur laquelle on dessine les images de l'animation, les unes sur les autres
        BufferedImage offimg = null;
        Graphics2D offg = null;
        if (deconeur.getFrameCount() > 1) {
            offimg = new BufferedImage(oldwidth, oldheight, BufferedImage.TYPE_INT_RGB);
            offg = offimg.createGraphics();
        }
        
        //System.out.println("rescale " + newf.getName() + " " + getWidth() + "x" + getHeight() + " -> " + width + "x" + height);
        
        boolean arriere_plan = (deconeur.getDispose() == 2);
        //System.out.println("revenir sur l'arrière-plan: " + arriere_plan);
        
        BufferedImage oldframe = null;
        //tt1=0;tt2=0;nt=0;
        for (int i=0; i<deconeur.getFrameCount(); i++) {
            if (deconeur.getFrameCount() > 1)
                System.out.print(".");
            BufferedImage frame = deconeur.getFrame(i);
            if (offg != null) {
                if (arriere_plan) {
                    offg.setColor(new Color(backrgb));
                    offg.fillRect(0, 0, oldwidth, oldheight);
                }
                offg.drawImage(frame, 0, 0, null);
            } else
                offimg = frame;
            // avec setIndexation true, on est sur que frame.getColorModel() instanceof IndexColorModel
            IndexColorModel icm = null;
            if (gicm != null)
                icm = gicm;
            else {
                icm = (IndexColorModel)frame.getColorModel();
                if (icm.getTransparentPixel() == -1 && deconeur.getFrameCount() > 1)
                    icm = icmTrans(icm);
            }
            int frameWidth = frame.getWidth(null);
            int frameHeight = frame.getHeight(null);
            
            // on n'utilisait pas de offg avec ce code en commentaire
            //byte[] frameData = ((DataBufferByte) frame.getRaster().getDataBuffer()).getData();
            
            //IndexColorModel ficm = (IndexColorModel)frame.getColorModel();
            //int[] fct = new int[ficm.getMapSize()];
            //ficm.getRGBs(fct);
            /*if (deconeur.getFrameCount() > 1 && ficm.getTransparentPixel() != -1) {
                // on retire la transparence avant le redimensionnement pour obtenir de jolis dégradés de gris
                // quand il y a du texte sur un fond transparent
                // (de toutes façons on perd la transparence avec l'optimisation)
                
                int indtransframe = ficm.getTransparentPixel();
                //System.out.print(" indtransframe="+indtransframe);
                byte indback = findClosest(backrgb, fct, indtransframe);
                //System.out.print(" indback="+(indback&0xFF));
                for (int j=0; j<frameWidth*frameHeight; j++)
                    if ((frameData[j] & 0xFF) == indtransframe)
                        frameData[j] = indback;
            }*/
            
            //BufferedImage frame2 = getMyScaledBufferedImage(frameData, fct, frameWidth, frameHeight,
            //    width, height, icm, (gct != null));
            BufferedImage frame2 = getMyScaledBufferedImage(offimg, frameWidth, frameHeight, width, height, icm);
            e.setDelay(deconeur.getDelay(i));
            // problème avec le délai quand il n'y a pas de GraphicControlExt dans le fichier de départ
            // car ce bloc apparait toujours dans le fichier résultat
            if (optimisation && deconeur.getFrameCount() > 1) {
                int[] position = new int[4];
                BufferedImage newframe = optimiser(frame2, oldframe, backrgb, icm, arriere_plan, position);
                oldframe = frame2;
                frame2 = newframe;
                if (icm != null) {
                    int indtrans = icm.getTransparentPixel();
                    if (indtrans != -1) {
                        Color transcol = new Color(icm.getRGB(indtrans));
                        e.setTransparent(transcol);
                        e.setTransparentIndex(indtrans);
                    }
                }
                e.setDispose(1); // do not dispose
                //System.out.print("("+position[0]+","+position[1]+" "+position[2]+"x"+position[3]+")");
                e.addFrame(frame2, position);
            } else {
                if (deconeur.getFrameCount() == 1 && icm != null) {
                    int indtrans = icm.getTransparentPixel();
                    if (indtrans != -1) {
                        Color transcol = new Color(icm.getRGB(indtrans));
                        e.setTransparent(transcol);
                        e.setTransparentIndex(indtrans);
                    }
                }
                e.addFrame(frame2);
            }
        }
        if (deconeur.getFrameCount() > 1)
            System.out.println();
        //System.out.println("temps1: " + (tt1/nt) + "ms");
        e.finish();
    }
    
    protected byte findClosest(int rgb, int[] ct, int transIndex) {
        if (ct == null) return -1;
        int r = (rgb >> 16) & 0x00FF;
        int g = (rgb >> 8) & 0x0000FF;
        int b = rgb & 0x000000FF;
        int minpos = 0;
        int dmin = 256 * 256 * 256;
        int ri, gi, bi;
        for (int i=0; i<ct.length; i++) {
            ri = (byte)((ct[i] >> 16) & 0x00FF);
            gi = (byte)((ct[i] >> 8) & 0x0000FF);
            bi = (byte)(ct[i] & 0x000000FF);
            int dr = r - (ri & 0xFF);
            int dg = g - (gi & 0xFF);
            int db = b - (bi & 0xFF);
            int d = dr * dr + dg * dg + db * db;
            if (i != transIndex && d < dmin) {
                dmin = d;
                minpos = i;
            }
        }
        return (byte)minpos;
    }
    
    public BufferedImage getMyScaledBufferedImage(Image img, int width1, int height1, int width, int height,
            IndexColorModel icm) {
        
        double scalew = (width*1.0)/width1;
        double scaleh =  (height*1.0)/height1;
        
        BufferedImage buffImage;
        buffImage = new BufferedImage(width1, height1, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2 = buffImage.createGraphics();
        g2.drawImage(img, 0, 0, null);
        g2.dispose();
        
        int transrgb2 = 0;
        boolean yatrans = false;
        int indtrans = -1;
        if (icm != null) {
            indtrans = icm.getTransparentPixel();
            if (indtrans == -1)
                System.out.println("pas de transparence(2)!");
            else {
                transrgb2 = icm.getRGB(indtrans);
                yatrans = true;
            }
        }
        BufferedImage buff2;
        int[] icmrgbs = null;
        Hashtable<Integer,Integer> cachepalette = null;
        if (icm == null)
            buff2 = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        else {
            buff2 = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, icm);
            icmrgbs = new int[icm.getMapSize()];
            icm.getRGBs(icmrgbs);
            cachepalette = new Hashtable<Integer,Integer>();
        }
        int[] rgbs = buffImage.getRGB(0, 0, width1, height1, null, 0, width1);
        int[] rgbs2 = new int[width*height];
        for (int ix=0; ix<width; ix++)
            for (int iy=0; iy<height; iy++) {
                double xm = (ix + 0.5) / scalew - 0.5;
                double ym = (iy + 0.5) / scaleh - 0.5;
                double dx1 = xm - 1/(2*scalew);
                double dx2 = xm + 1/(2*scalew);
                double dy1 = ym - 1/(2*scaleh);
                double dy2 = ym + 1/(2*scaleh);
                int x1 = (int)Math.floor(dx1 + 0.5);
                if (x1 < 0)
                    x1 = 0;
                int x2 = (int)Math.floor(dx2 + 0.5);
                if (x2 > width1-1)
                    x2 = width1-1;
                int y1 = (int)Math.floor(dy1 + 0.5);
                if (y1 < 0)
                    y1 = 0;
                int y2 = (int)Math.floor(dy2 + 0.5);
                if (y2 > height1-1)
                    y2 = height1-1;
                double bx1 = x1 + 0.5 - dx1;
                double bx2 = dx2 - x2 + 0.5;
                double by1 = y1 + 0.5 - dy1;
                double by2 = dy2 - y2 + 0.5;
        //System.out.println("x1="+x1+" x2="+x2+" y1="+y1+" y2="+y2);
                double tred=0;
                double tgreen=0;
                double tblue=0;
                double tnb=0;
                int rgb;
                double f;
                for (int ix2=x1; ix2<=x2; ix2++)
                    for (int iy2=y1; iy2<=y2; iy2++) {
                        rgb = rgbs[iy2*width1 + ix2];
                        f = 1;
                        if (ix2 == x1)
                            f *= bx1;
                        if (ix2 == x2)
                            f *= bx2;
                        if (iy2 == y1)
                            f *= by1;
                        if (iy2 == y2)
                            f *= by2;
                        tred += f * ((rgb >> 16) & 0xFF);
                        tgreen += f * ((rgb >> 8) & 0xFF);
                        tblue += f * (rgb & 0xFF);
                        tnb += f;
                    }
                tred = tred / tnb;
                tgreen = tgreen / tnb;
                tblue = tblue / tnb;
        //System.out.println(ix+","+iy+" : tnb="+tnb+" tred="+tred+" tgreen="+tgreen+" tblue="+tblue);
                int r = (int)tred;
                int g = (int)tgreen;
                int b = (int)tblue;
                rgb = 0xFF000000 | (r << 16) | (g << 8) | b;
                if (icm != null) {
                    // recherche de la couleur la plus proche dans la palette
                    Integer cachei = cachepalette.get(new Integer(rgb));
                    if (cachei != null)
                        rgb = cachei.intValue();
                    else {
                        int besti = -1;
                        long bestdiff = -1;
                        for (int i=0; i<icmrgbs.length; i++)
                            if (!yatrans || i != indtrans) {
                                int rgbi = icmrgbs[i];
                                int ri = (rgbi >> 16) & 0xFF;
                                int gi = (rgbi >> 8) & 0xFF;
                                int bi = rgbi & 0xFF;
                                long diff = (ri - r)*(ri - r) + (gi - g)*(gi - g) + (bi - b)*(bi - b);
                                if (besti == -1 || diff < bestdiff) {
                                    besti = i;
                                    bestdiff = diff;
                                }
                            }
                        if (besti != -1) {
                            cachepalette.put(new Integer(rgb), new Integer(icmrgbs[besti]));
                            rgb = icmrgbs[besti];
                        }
                    }
                }
                int rgbm = rgbs[(int)Math.round(ym)*width1 + (int)Math.round(xm)];
                if ((rgbm & 0xFF000000) == 0) {
                    if (yatrans)
                        rgb = transrgb2;
                    else
                        rgb = rgb & 0x00FFFFFF;
                }
                rgbs2[iy*width + ix] = rgb;
            }
        buff2.setRGB(0, 0, width, height, rgbs2, 0, width);
        return(buff2);
    }
    
    public IndexColorModel icmTrans(IndexColorModel icm) {
        IndexColorModel icm2;
        // on ajouter une couleur transparente ou on en modifie une pour qu'elle devienne transparente
        if (icm.getMapSize() < 256) {
            int count = icm.getMapSize() + 1;
            System.out.print("ajout couleur (count="+count+")");
            byte reds[] = new byte[count];
            byte greens[] = new byte[count];
            byte blues[] = new byte[count];
            icm.getReds(reds);
            icm.getGreens(greens);
            icm.getBlues(blues);
            for (int i=count-1; i>0; i--) {
                reds[i] = reds[i-1];
                greens[i] = greens[i-1];
                blues[i] = blues[i-1];
            }
            reds[0] = (byte)0xFF;
            greens[0] = (byte)0xFF;
            blues[0] = (byte)0xFF;
            icm2 = new IndexColorModel(8, count, reds, greens, blues, 0);
        } else {
            // on demande un volontaire pour devenir transparent
            int count = icm.getMapSize();
            int indtrans = -1;
            long dist = -1;
            for (int i=0; i<count; i++) {
                int rgbi = icm.getRGB(i);
                int ri = (rgbi >> 16) & 0x00FF;
                int gi = (rgbi >> 8) & 0x0000FF;
                int bi = rgbi & 0x000000FF;
                for (int j=i+1; j<count; j++) {
                    int rgbj = icm.getRGB(j);
                    int rj = (rgbj >> 16) & 0x00FF;
                    int gj = (rgbj >> 8) & 0x0000FF;
                    int bj = rgbj & 0x000000FF;
                    long diff = (ri - rj)*(ri - rj) + (gi - gj)*(gi - gj) + (bi - bj)*(bi - bj);
                    if (dist == -1 || diff < dist) {
                        dist = diff;
                        indtrans = i;
                    }
                }
            }
            //System.out.print("volontaire transparent: " + indtrans + " dist="+dist+" ");
            //System.out.println("rgb=" + Long.toString(icm.getRGB(indtrans) & 0xFFFFFFFFL, 16).toUpperCase());
            byte reds[] = new byte[count];
            byte greens[] = new byte[count];
            byte blues[] = new byte[count];
            icm.getReds(reds);
            icm.getGreens(greens);
            icm.getBlues(blues);
            icm2 = new IndexColorModel(icm.getPixelSize(), count, reds, greens, blues, indtrans);
        }
        return(icm2);
    }
    
    public BufferedImage optimiser(BufferedImage frame, BufferedImage oldframe, int backrgb,
        IndexColorModel icm2, boolean arriere_plan, int[] position) throws IOException {
        int width = frame.getWidth();
        int height = frame.getHeight();
        //System.out.println("optimiser width="+width+" height="+height);
        // icm ici servirait si on voulait mettre une palette par image quand il n'y a pas de palette globale
        //IndexColorModel icm = null;
        //if (frame.getColorModel() instanceof IndexColorModel)
        //    icm =  (IndexColorModel)frame.getColorModel();
        if (oldframe != null) {
            // détermination de la zone de différences
            int xmin = -1;
            int ymin = -1;
            int xmax = -1;
            int ymax = -1;
            for (int ix=0; ix<width; ix++)
                for (int iy=0; iy<height; iy++) {
                    if (frame.getRGB(ix, iy) != oldframe.getRGB(ix, iy)) {
                        if (xmin == -1 || xmin > ix)
                            xmin = ix;
                        if (ymin == -1 || ymin > iy)
                            ymin = iy;
                        if (xmax == -1 || xmax < ix)
                            xmax = ix;
                        if (ymax == -1 || ymax < iy)
                            ymax = iy;
                    }
                }
            if (xmin == -1) {// aucune différence -> on met juste un pixel
                xmin = 0;
                ymin = 0;
                xmax = 0;
                ymax = 0;
            }
            int width2 = xmax - xmin + 1;
            int height2 = ymax - ymin + 1;
            //System.out.println("width2="+width2+" height2="+height2);
            // création petite image
            BufferedImage frame2;
            int transrgb = -1;
            int indtrans;
            if (icm2 == null) {
                /*
                if (icm != null) {
                    indtrans = icm.getTransparentPixel();
                    if (indtrans != -1) {
                        transrgb = icm.getRGB(indtrans);
                        icm2 = icm;
                    } else {
                        icm2 = icmTrans(icm);
                        indtrans = icm2.getTransparentPixel();
                        transrgb = icm2.getRGB(indtrans);
                    }
                }
                */
                frame2 = new BufferedImage(width2, height2, BufferedImage.TYPE_INT_ARGB);
            } else {
                indtrans = icm2.getTransparentPixel();
                if (indtrans != -1)
                    transrgb = icm2.getRGB(indtrans);
                frame2 = new BufferedImage(width2, height2, BufferedImage.TYPE_BYTE_INDEXED, icm2);
            }
            int[] rgbs = frame.getRGB(xmin, ymin, width2, height2, null, 0, width2);
            for (int ix=0; ix<width2; ix++)
                for (int iy=0; iy<height2; iy++) {
                    int rgb = rgbs[iy*width2 + ix];
                    int orgb = oldframe.getRGB(ix+xmin, iy+ymin);
                    //if (rgb == orgb || ((rgb & 0xFF000000) == 0 && (orgb & 0xFF000000) == 0)) {
                    if (rgb == orgb) {
                        if (transrgb == -1)
                            rgbs[iy*width2 + ix] = rgb & 0x00FFFFFF; // le pixel doit être transparent
                        else
                            rgbs[iy*width2 + ix] = transrgb;
                    } else if (/*arriere_plan &&*/ (rgb & 0xFF000000) == 0) { // commentaire pour windoz bug
                        rgbs[iy*width2 + ix] = rgb | 0xFF000000; // le pixel doit être opaque
                    }
                }
            frame2.setRGB(0, 0, width2, height2, rgbs, 0, width2);
            
            position[0] = xmin;
            position[1] = ymin;
            position[2] = width2;
            position[3] = height2;
            return(frame2);
        } else {
            // la première image doit être opaque
            BufferedImage frame2;
            if (icm2 == null)
                frame2 = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
            else
                frame2 = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, icm2);
            
            Graphics2D g2 = frame2.createGraphics();
            if (!System.getProperty("os.name").startsWith("Mac OS") || "10.5".compareTo(System.getProperty("os.version")) <= 0)
                g2.setComposite(AlphaComposite.Src);// windoz bug workaround
            //g2.setColor(new Color(backrgb));
            //g2.fillRect(0, 0, width, height);
            g2.drawImage(frame, 0, 0, null);
            
            position[0] = 0;
            position[1] = 0;
            position[2] = width;
            position[3] = height;
            
            return(frame2);
        }
    }
    
}
