Package giftoapng

Source Code of giftoapng.APNG

/**
* Copyright: t3am_C9 (Alexander Schäffer, Johannes Ebersold, Sebastian Geib, Thomas Kisiel)
*
* This file is part of GifToApngConverter
*
* GifToApngConverter is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* GifToApngConverter 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GifToApngConverter. If not, see <a href="http://www.gnu.org/licenses/">here</a>
*/

package giftoapng;


import gif.GifInfo;
import gui.I_UIupdater;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Vector;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;


import com.sun.imageio.plugins.gif.GIFImageMetadata;
import com.sun.imageio.plugins.gif.GIFImageReader;
import com.sun.imageio.plugins.gif.GIFStreamMetadata;

/**
* Represents an APNG, is empty at creation and is fillable by addframe() or you can
* use the convertGiftoAPNG()-method to read an animated Gif-file into an APNG-object.
* @author Alexander Sch&auml;ffer
* @author Thomas Kisiel
*/
public class APNG {

  private ArrayList<Chunk> maChunks=new ArrayList<Chunk>();
  private Vector<Chunk> maTmpChunkList = new Vector<Chunk>();
  private boolean mbReadyToWrite=false;
  private int miNumFrames=0;
  private final Emodes meMode;
  private ChunkacTL mcAniControl; //keep chunk to tell the number of frame in prepareToWrite()
  private ChunkIDAT mChunkIDATPointer=null; // keeps track of the generated idat file
 
  /**
   * Enumerates the modes for the APNG-class.
   * It controls the image-conversion and which chunks are needed.
   * @author Alexander Sch&auml;ffer
   *
   */
  public enum Emodes {PALETTE,RGB,ARGB};
 
  /**
   * creates a emtpy APNG and adds necessary chunks, like IHDR(header)
   *  and acTL(animation control). If mode is PALETTE then PLTE(palette)
   *   and tRNS(transparecy) -chunks are added between IHDR and acTL
   *
   * @param width width of the first frame
   * @param height height of the first frame
   * @param mode palette, truecolor(RGB) or truecolor+alpha(ARGB); cannot be changed later
   * @param numloops the animation stops after this number of iterations
   * @param palette colortable in RGB-byte-order with length<=768 and multiple of 3.
   *         If mode is <b>not</b> PALETTE then it will be ignored and may be <i>null</i>
   * @param transparentkey the index of the palette, which will be transparent.
   *         If mode is <b>not</b> PALETTE then it will be ignored and may be <i>-1</i>
   */
  public APNG(int width, int height, Emodes mode, int numloops, byte[] palette, int transparentkey){
    meMode=mode;
   
    int colormode=0; //values according to PNG-spec:
             //  2=truecolor, 3=indexed colors, 6=truecolor+alpha
    switch(meMode){ 
    case PALETTE:
      colormode=3;
      break;
    case RGB:
      colormode=2;
      break;
    case ARGB:
      colormode=6;
      break;
    default:
      //doesn't happen
      break;
    }
   
    //add header-chunk
    maChunks.add(new ChunkIHDR(width,height,(byte)colormode));
   
    //add palette according to mode
    if(meMode==Emodes.PALETTE){
      //add PLTE-chunk
      maChunks.add(new ChunkPLTE(palette));
     
      maChunks.add(new ChunktRNS(transparentkey));
    }
   
    mcAniControl=new ChunkacTL(numloops); //keep chunk to tell the number of frame in prepareToWrite()
    maChunks.add(mcAniControl);

  }
 
  /**
   * add the IEND-chunk after the frame-chunks
   *and tell the acTL how many frames the animation finally has
   */
  public void prepareToWrite(){
   
    if(!mbReadyToWrite){
      //add IEND-chunk
      maChunks.add(new ChunkIEND());
     
      //tell acTL the number of frames
      mcAniControl.setNumFrames(miNumFrames);
      mbReadyToWrite=true;
    }
  }
 
  /**
   * adds the image with offset (0,0) and minimal frame-delay to the animation
   * @see #addFrame(BufferedImage, int, int, int, int)
   * @param imagedata
   */
  public void addFrame(BufferedImage imagedata){
    addFrame(imagedata,0,0,0,1,0);
  }
 
  /**
   * Adds the fcTL(frame-control), then
   * converts the imagedata according to the <i>meMode</i> to the png-image-format and
   * it inside a FDAT(for frame#1) or a fdAT(for following frames) to the chunk-array.
   *
   * @param imagedata
   * @param x_offset to offset a tile in x-direction within the frame of the animation
   * @param y_offset same in y-direction
   * @param delaytime how long this frame should be shown (in 1/100ths seconds)
   * @param disposalmethod 0=no disposal, 1=restore to background, 2=restore to previous
   */
  public void addFrame(BufferedImage imagedata, int x_offset, int y_offset, int delaytime,
                   int disposalmethod, int sequenceNr){

    if(!mbReadyToWrite){
      int width=imagedata.getWidth();
      int height=imagedata.getHeight();
      int blendmethod;
      byte filterByte = 0;
      //blend-mode "OVER" darf nur im ARGB-modus benutzt werden
      if(meMode==Emodes.ARGB){
        blendmethod=ChunkfcTL.APNG_BLEND_OP_OVER;
      } else {
        blendmethod=ChunkfcTL.APNG_BLEND_OP_SOURCE;
      }
     
      // if we have to deal with RGB and ARGB data, then use filter type 4
      if (meMode == Emodes.RGB) filterByte = 4// filter with peathPredictor
     
      //add fcTL-chunk
      ChunkfcTL fctl = new ChunkfcTL(sequenceNr, width, height, x_offset, y_offset,
                      delaytime, disposalmethod,blendmethod);
      sortInto(fctl, maTmpChunkList);
     
     
      //refine the BufferedImage to a byte-array according to the mode
      //deflate-compression handles the chunk
      byte[] image={};
      if(meMode==Emodes.PALETTE){
        int[] indices=imagedata.getRaster().getSamples(0, 0, width,
                    height, 0, (int[])null);
        //each index is 8 bit
        image=new byte[indices.length+imagedata.getHeight()];
        int read=0;
        for(int i=0;i<image.length;i++){
          if(i%(width+1)==filterByte){ //filterbyte
            image[i]=0;
          } else {
            image[i]=(byte)indices[read];
            read++;
          }
        }
             
      } else {
       
        int byte_per_pixel;
        if (meMode == Emodes.RGB)
        {
          byte_per_pixel=3;
        }
        else
        {
          byte_per_pixel=4;
        }
        
        //image=new byte[width*height*byte_per_pixel+imagedata.getHeight()];
        image=new byte[width*height*byte_per_pixel];
       
        int[] colorValues=imagedata.getRGB(0, 0, width, height, null, 0, width);
       
        int i = 0;
        for (int color : colorValues)
        {
          // split color value
          image[i+2]=(byte)color;  //blue
          color>>=8;
          image[i+1]=(byte)color;  //green
          color>>=8;
          image[i]=(byte)color;    //red
          if (meMode == Emodes.ARGB)
          {
            color>>=8;
            image[i+3]=(byte)color;  //alpha
          }
          i += byte_per_pixel;
        }
       
        // filter the byte array
        image = Filter.applyFilter(image, width,imagedata.getHeight(),byte_per_pixel,(byte) 0);
      }
     
      //if stop-button is clicked
      if(Thread.currentThread().isInterrupted()){
        return;
      }
     
      if(sequenceNr==0){
        //first frame is IDAT-chunk. Will be generated only once. We will keep track
        // off it and won't add it to the list because it has no seq number and it
        // makes sorting harder if we add it now
        mChunkIDATPointer = new ChunkIDAT(image);
      } else {
        //additional frames are fdAT-chunks
        //maTmpChunkList.add(new ChunkfdAT(sequenceNr+1,image));
        sortInto(new ChunkfdAT(sequenceNr+1,image), maTmpChunkList);
      }
      miNumFrames++;
    } else {
      throw new IllegalStateException("APNG is ready to write: no more frames can be added");
    }
  }
 

  /**
   * Will read, analyze the gif, create the APNG-object and adds the frames to it .
   * The calling method should handle the exception.
   * @param readGif
   * @return the generated APNG-object
   * @throws IOException
   */
  public static APNG convertGiftoAPNG(File readGif) throws IOException{
    return convertGiftoAPNG(readGif,null);
  }
 
  /**
   * Will read, analyze the gif, create the APNG-object ands adds the frames to it.
   * The calling method should handle the exception.
   * @param readGif
   * @param callback callback-methode, not used if null
   * @return the generated APNG-object
   * @throws IOException
   */
  public static APNG convertGiftoAPNG(File readGif,I_UIupdater callback) throws IOException{
    APNG theApng=null;
    FileInputStream fis=null;
    GIFImageReader reader=null;
    try{
      fis=new FileInputStream(readGif);
     
      reader = (GIFImageReader) ImageIO.getImageReadersByFormatName("GIF").next();
          ImageInputStream iis = ImageIO.createImageInputStream(fis);
          reader.setInput(iis);
         
          int numImages=reader.getNumImages(true);
         
          boolean hasLocalColors=false;
          boolean hasTransparecy=false;
          int iTransparency=-1;
                     
          GIFStreamMetadata smd=(GIFStreamMetadata) reader.getStreamMetadata();
         
          //Analyze, which mode will be used
          GIFImageMetadata md;
          for(int i=0;i<numImages;i++){
            md=(GIFImageMetadata) reader.getImageMetadata(i);
            if(md.transparentColorFlag){
              iTransparency=md.transparentColorIndex;
              hasTransparecy=true;
            }
            if(md.localColorTable!=null){
              hasLocalColors=true;
            }               
           
          }
          Emodes mode=Emodes.PALETTE; // for no local Colortables and no/single-transparency
                        // and NO animation
          int decision=0;
          if(hasLocalColors){
            decision+=1;
          }
          if(hasTransparecy){
            decision+=2;
          }
          if((numImages>1)){
            decision+=4;
          }
 
          switch(decision){
         
          default:
          case 0: case 2:
            mode=Emodes.PALETTE;
            break;
          case 1: case 4: case 5:
            mode=Emodes.RGB;
            break;
          case 3: case 6: case 7
            mode=Emodes.ARGB;
            break;
          }
          md=(GIFImageMetadata) reader.getImageMetadata(0);
          if(md.imageLeftPosition>0 || md.imageTopPosition>0
              || md.imageHeight!=smd.logicalScreenHeight
              || md.imageWidth!=smd.logicalScreenWidth){
          // first frame must have no offset!
            // mode set to ARGB, image will be altered before addFrame()
            mode=Emodes.ARGB;
          }
         
         
 
 
          int numLoops=ChunkacTL.INFINITE_LOOP;
          GifInfo readgifinfo=new GifInfo(readGif);
        numLoops=readgifinfo.getLoops();
 
        theApng=new APNG(smd.logicalScreenWidth,smd.logicalScreenHeight,mode, numLoops,
                       (mode==Emodes.PALETTE)? smd.globalColorTable : null, iTransparency);
         
          //now add frames (by using BufferedImage)
         
          //tell callback how many frame there are
          if(callback!=null){
            callback.updatemax(numImages);
          }
         
          // prepare our workers
          ExecutorService threadExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
         
          // Disposal-Methods:
          // method name(gif-method-number -> apng-method-number):
          //  unknown(0->0(ignored)), no disposal(1->0)
          //  restore to background(2->1), restore to previous(3->2)
          int seqNr = 0;
          for(int i=0;i<numImages;i++){
           
            //if stop-button is clicked
        if(Thread.currentThread().isInterrupted()){
          //close reader & stream
                reader.dispose();
                fis.close();
                throw new CancellationException("stop-button was pushed");
        }
       
            //callback current frame:
              if(callback!=null){
                callback.updatecur(i+1);
              }   
           
            md=(GIFImageMetadata) reader.getImageMetadata(i);
            int idismet=md.disposalMethod;
            if(idismet>0 && idismet<=3){
              idismet=idismet-1;
            }
           
           
           
            if(i==0 && (md.imageLeftPosition>0 || md.imageTopPosition>0
                  || md.imageHeight!=smd.logicalScreenHeight
                  || md.imageWidth!=smd.logicalScreenWidth)){
              // first frame must have no offset!
              BufferedImage tmp=new BufferedImage(smd.logicalScreenWidth,
                        smd.logicalScreenHeight,BufferedImage.TYPE_INT_ARGB);
 
              Graphics tmpg=tmp.getGraphics();
              tmpg.setColor(new Color(0,0,0,0));
              tmpg.fillRect(0, 0, smd.logicalScreenWidth, smd.logicalScreenHeight);
              tmpg.drawImage(reader.read(i), md.imageLeftPosition, md.imageTopPosition, null);
             
              //theApng.addFrame(tmp, 0, 0, md.delayTime, idismet);
              FrameThread ft = new FrameThread(theApng,tmp, 0, 0, md.delayTime, idismet,seqNr);
              threadExecutor.execute(ft);
            } else {
              FrameThread ft = new FrameThread(theApng,reader.read(i), md.imageLeftPosition, md.imageTopPosition,md.delayTime, idismet,seqNr);
              threadExecutor.execute(ft);
            }
           
            // incr seqnr
            if (i == 0)
            {
              seqNr++;  // first idat has no seqnr
            }
            else
            {
              seqNr+=2;
            }
        
          } // add all frames
         
          // all threads should come to an end now
          threadExecutor.shutdown(); // shutdown worker threads
          while (!threadExecutor.isTerminated())
          {
            try
        {
          threadExecutor.awaitTermination(60,TimeUnit.SECONDS);
        }
        catch (InterruptedException e)
        {
          e.printStackTrace();
        }
          }
          // seperate chunk list has to be now integrated
          // this has to happen because of the parallel working threads
          theApng.integrateChunks();
         
          //add tEXt-chunk here
          theApng.maChunks.add(new ChunktEXt()); //software-note
          //and all comments from the gif:
          theApng.maChunks.add(new ChunktEXt("Comment",readgifinfo.getComment()));
 
          //close reader & stream
          reader.dispose();
          fis.close();
         
          //callback completion of all frames / beginning of write-operation, which should follow
            if(callback!=null){
              callback.updatecur(numImages+1);
            }  
           
    } catch (IOException ex){
      //close reader & stream
      if(reader!=null){
        reader.dispose();
      }
      if(fis!=null){
        fis.close();
      }
          throw ex;//and pass through
    } catch (RuntimeException ex){
      //close reader & stream
      if(reader!=null){
        reader.dispose();
      }
      if(fis!=null){
        fis.close();
      }
          throw ex;//and pass through
    }
   
    return theApng;
   
  }
 

  /**
   * Writes all chunks of the array into the given BufferedOutputStream.
   * The calling method should handle the exception.
   * @param bos
   * @throws IOException
   */
  public void write(BufferedOutputStream bos) throws IOException{
    if(!mbReadyToWrite){
      prepareToWrite();
    }
   
    Iterator<Chunk> allchunks=maChunks.iterator();
   
    while(allchunks.hasNext()){
      allchunks.next().write(bos);
    }
   
   
  }
 
  /**
   * Add the missing idat chunk and integrate the list into the main chunklist
   */
  private void integrateChunks()
  {
    // add our idat chunk to index 1
    maTmpChunkList.add(1, mChunkIDATPointer);
    // now copy the list into our real chunk list
    for (Chunk c:maTmpChunkList)
    {
      maChunks.add(c);
    }
  }
 
  /**
   * Sorts a chunk into a Vector depending on the sequence number
   * @param source
   * @param list
   */
  private synchronized void sortInto(Chunk source, Vector<Chunk> list)
  {
    if (list.size() <= 0)
    {
      list.add(source);
    }
    else
    {
      int seq = ((iHasSequence)source).getSequenceNumber();
      for (int i = list.size()-1; i >= 0;i--)
      {
        iHasSequence s = (iHasSequence) list.get(i);
        if (s.getSequenceNumber() < seq)
        {
          list.add(i+1,source);
          return;
        }
      }
      // nothing found so add to the beginning
      list.add(0,source);
    }
  }

}

TOP

Related Classes of giftoapng.APNG

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.