/**
* 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ä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ä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);
}
}
}