Hatena::ブログ(Diary)

明日の鍵

2010-06-18

GifAnalyzerを公開します。

GifデコーダやらGifエンコーダやら作っていると、どうしてもGifファイルをバイナリで見ないといけません。

バイナリエディタで見るのもいいんですが、たいへんです。つかれます。

そこで、Gifファイルを読んで各属性を出力するプログラム書いて使ってました。

こんなコードは共有するに限ると思ったので、公開します。

ゴリゴリ書いてますので、バグってる事もあるかもしれません。

機能はコメントに書かれている通りです。

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.FileOutputStream;
import java.io.File;

/**
 * GIFファイルを分解する
 * GIFファイルを分解して、各属性を表示する
 * 属性が表示されるブロックはヘッダとアプリケーション拡張とグラフィックコントロール拡張
 * テキスト拡張とコメント拡張は、バイトデータのみ表示される
 * GIFアニメーションを分解して、1枚1枚の画像に保存する
 * inputフォルダ内のGIFファイルを読み取り、outputフォルダに画像を保存する
 * 拡張子のチェックと先頭3バイトのチェックをしている、拡張子がGIFでも中身がPNGとかだと、エラーで落ちる
 * 対応しているファイルサイズは256KB
 * バッファサイズを変更すれば大きくすることもできる
 */
public class GifAnalyzer {
  private static final String IN_DIR = "input";
  private static final String OUT_DIR = "output";

  public static void main(String[] args) throws Exception {
    File file = new File(IN_DIR);
    String[] files = file.list();
    for(int i=0;i<files.length;i++) {
      if(!files[i].toUpperCase().endsWith(".GIF"))
        continue;
      System.out.println("ファイル:" + files[i]);
      GifResolver resolver = new GifResolver(IN_DIR + "\\" + files[i]);
      resolver.resolve(OUT_DIR + "\\" + files[i]);
    }
  }
}

class GifResolver {
  private static final byte TRR_CODE = (byte)0x3B;
  private static final byte IMG_CODE = (byte)0x2C;
  private static final byte EXT_CODE = (byte)0x21;
  private static final byte GC_EXT = (byte)0xF9;
  private static final byte APP_EXT = (byte)0xFF;
  private static final byte CMT_EXT = (byte)0xFE;
  private static final byte TXT_EXT = (byte)0x01;
  private static final int BUFFER_SIZE = 256 * 1024;

  private String path;
  private int offset = 0;
  
  private GifHeader gifHeader;
  private ApplicationExtension appExt;
  private GraphicControlExtension gcExt;
  private CommentExtension cmtExt;
  private PlainTextExtension txtExt;
  private ImageBlock imageBlock;

  public GifResolver(String path){
    this.path = path;
  }

  public void resolve(String dir) throws FileNotFoundException , IOException {
    // ファイルを取得
    File f = new File(path);
    FileInputStream is = new FileInputStream(f);
    byte[] buffer = new byte[BUFFER_SIZE];
    long length = f.length();
    
    if(BUFFER_SIZE < length){
      System.out.println("バッファサイズが足りません");
      System.out.println("用意されているバッファサイズ -> " + BUFFER_SIZE + "Byte");
      System.out.println("必要なバッファサイズ -> " + length + "Byte");
      throw new IOException();
    }
    
    is.read(buffer);
    is.close();
    
    //分解開始
    int no = 0;

    // ヘッダを取得
    System.out.println("--Gif Header--");
    gifHeader = new GifHeader(buffer, offset);
    offset += gifHeader.size;
    
    if(!gifHeader.getSignature().equals("GIF")){
      System.out.println("GIFではありません");
      throw new IOException();
    }
    
    // ヘッダ内容を表示
    System.out.println("Signature:" + gifHeader.getSignature());
    System.out.println("Version:" + gifHeader.getVersion());
    System.out.println("Width:" + gifHeader.getWidth());
    System.out.println("Height:" + gifHeader.getHeight());
    System.out.println("GlobalColorTableFlag:" + gifHeader.getGlobalColorTableFlag());
    System.out.println("ColorResolution:" + gifHeader.getColorResolution());
    System.out.println("SortFlag:" + gifHeader.getSortFlag());
    System.out.println("SizeOfGlobalColorTable:" + gifHeader.getSizeOfGlobalColorTable());
    System.out.println("BackgroundColorIndex:" + gifHeader.getBackgroundColorIndex());
    System.out.println("PixelAspectRatio:" + gifHeader.getPixelAspectRatio());
    int[] colors = gifHeader.getGlobalColorTable();
    for(int i=0;i<colors.length;i++){
      System.out.println("colors[" + i + "]:0x" + Util.toHex(colors[i] , 6));
    }
    // バイト配列を表示
    Util.dump(gifHeader.bytes, gifHeader.size);
    
    // トレーラが来るまでループ
    while(buffer[offset] != TRR_CODE) {
      if(buffer[offset] == IMG_CODE) {
        // ImageBlock
        System.out.println("--ImageBlock--");
        imageBlock = new ImageBlock(buffer, offset);
        offset += imageBlock.size;
        
        // 内容を表示
        System.out.println("ImageSeparator:" + imageBlock.getImageSeparator());
        System.out.println("ImageLeftPosition:" + imageBlock.ImageLeftPosition());
        System.out.println("ImageTopPosition:" + imageBlock.getImageTopPosition());
        System.out.println("ImageWidth:" + imageBlock.getImageWidth());
        System.out.println("ImageHeight:" + imageBlock.getImageHeight());
        System.out.println("LocalColorTableFlag:" + imageBlock.getLocalColorTableFlag());
        System.out.println("InterlaceFlag:" + imageBlock.getInterlaceFlag());
        System.out.println("SortFlag:" + imageBlock.getSortFlag());
        System.out.println("Reserved:" + imageBlock.getReserved());
        System.out.println("SizeOfLocalColorTable:" + imageBlock.getSizeOfLocalColorTable());
        colors = imageBlock.getLocalColorTable();
        for(int i=0;i<colors.length;i++){
          System.out.println("colors[" + i + "]:" + Util.toHex(colors[i], 6));
        }
        System.out.println("LZWMinimumCodeSize:" + imageBlock.getLZWMinimumCodeSize());
        
        //バイト配列を表示
        Util.dump(imageBlock.bytes, imageBlock.size);
        
        //ファイルに出力する
        outputImage(dir + no + ".gif");
        // ファイル名を更新する
        no++;
        
      } else if(buffer[offset] == EXT_CODE) {
        if(buffer[offset + 1] == GC_EXT) {
          //GraphicControlExtension
          System.out.println("--GraphicControlExtension--");
          gcExt = new GraphicControlExtension(buffer, offset);
          offset += gcExt.size;
          
          // 内容を表示
          System.out.println("ExtensionIntroducer:0x" + Util.toHex(gcExt.getExtensionIntroducer(), 2));
          System.out.println("GraphicControlLabel:0x" + Util.toHex(gcExt.getGraphicControlLabel(), 2));
          System.out.println("BlockSize:" + gcExt.getBlockSize());
          System.out.println("Reserved:" + gcExt.getReserved());
          System.out.println("DisposalMothod:" + gcExt.getDisposalMothod());
          System.out.println("UserInputFlag:" + gcExt.getUserInputFlag());
          System.out.println("TransparentColorFlag:" + gcExt.getTransparentColorFlag());
          System.out.println("DelayTime:" + gcExt.getDelayTime());
          System.out.println("TransparentColorIndex:" + gcExt.getTransparentColorIndex());
          
          // バイト配列を表示
          Util.dump(gcExt.bytes, gcExt.size);
        }else if(buffer[offset + 1] == APP_EXT){
          //ApplicationExtension
          System.out.println("--Application Extension--");
          appExt = new ApplicationExtension(buffer, offset);
          offset += appExt.size;
          
          // 内容を表示
          System.out.println("getExtensionIntroducer:0x" + Util.toHex(appExt.getExtensionIntroducer() , 2));
          System.out.println("getExtensionLabel:0x" + Util.toHex(appExt.getExtensionLabel(), 2));
          System.out.println("getBlockSize1:0x" + Util.toHex(appExt.getBlockSize1(), 2));
          System.out.println("getApplicationIdentifier:" + appExt.getApplicationIdentifier());
          System.out.println("getApplicationAuthenticationCode:" + appExt.getApplicationAuthenticationCode());
          
          // バイト配列を表示
          Util.dump(appExt.bytes,appExt.size);
          
        }else if(buffer[offset + 1] == CMT_EXT){
          //CommentExtension
          System.out.println("--CommentExtension--");
          cmtExt = new CommentExtension(buffer, offset);
          offset += cmtExt.size;
          Util.dump(cmtExt.bytes,cmtExt.size);
        }else if(buffer[offset + 1] == TXT_EXT){
          //PlainTextExtension
          System.out.println("--PlainTextExtension--");
          txtExt = new PlainTextExtension(buffer, offset);
          offset += txtExt.size;
          Util.dump(txtExt.bytes,txtExt.size);
        }else{
          System.out.println("不明な拡張(" + buffer[offset + 1] + ")");
          throw new IOException();
        }
      } else {
        System.out.println("不明なブロック(" + buffer[offset] + ")");
        throw new IOException();
      }
    }
    
    if((offset + 1) != length){
      System.out.println("すべて読み取る事ができませんでした");
      throw new IOException();
    }
  }
  
  /**
   * ファイルに出力する
   */
  private void outputImage(String path) throws FileNotFoundException , IOException {
    FileOutputStream stream = new FileOutputStream(path);
    stream.write(gifHeader.bytes);
    if(appExt != null)
      stream.write(appExt.bytes);
    if(gcExt != null)
      stream.write(gcExt.bytes);
    stream.write(imageBlock.bytes);
    stream.write((byte)0x3B);
    stream.close();
  }
}

class GifHeader {
  public byte[] bytes;
  public int size;
  public GifHeader(byte[] bytes, int offset){
    /* GlobalColorTableを持っているかフラグ */
    boolean globalColorTableFlag = (bytes[offset + 0x0A] & 0x80) != 0x00;
    /* GlobalColorTableのサイズ */
    int globalColorTableSize = (bytes[offset + 0x0A] & 0x07);
    
    // サイズを決める
    size = 0x0D;
    if(globalColorTableFlag)
      size += Math.pow(2, (globalColorTableSize + 1)) * 3;
    
    // バイト列のコピー
    this.bytes = Util.subarray(bytes, offset, size);
  }
  public String getSignature() {
    return new String(bytes, 0, 3);
  }
  public String getVersion() {
    return new String(bytes, 3, 3);
  }
  public int getWidth(){
    return (bytes[6] & 0xFF) + ((bytes[7] & 0xFF) << 8);
  }
  public int getHeight(){
    return (bytes[8] & 0xFF) + ((bytes[9] & 0xFF) << 8);
  }
  public int getGlobalColorTableFlag(){
    return (bytes[10] & 0x80) >> 7;
  }
  public int getColorResolution(){
    return (bytes[10] & 0x70) >> 4;
  }
  public int getSortFlag(){
    return (bytes[10] & 0x08) >> 3;
  }
  public int getSizeOfGlobalColorTable(){
    return (bytes[10] & 0x07);
  }
  public int getBackgroundColorIndex(){
    return bytes[11] & 0xFF;
  }
  public int getPixelAspectRatio(){
    return bytes[12];
  }
  public int[] getGlobalColorTable(){
    if(getGlobalColorTableFlag() == 0)
      return new int[0];
    int[] colors = new int[(int)Math.pow(2, getSizeOfGlobalColorTable() + 1)];
    for(int i=0;i<colors.length;i++)
      colors[i] = ((bytes[13 + (i * 3)] & 0xFF) << 16) + ((bytes[13 + (i * 3) + 1] & 0xFF) << 8) + (bytes[13 + (i * 3) + 2] & 0xFF);
    return colors;
  }
}

class ImageBlock {
  public byte[] bytes;
  public int size;
  public ImageBlock(byte[] bytes, int offset){
    int blockSize;
    /* LocalColorTableFlagを持っているかフラグ */
    boolean localColorTableFlag = (bytes[offset + 0x09] & 0x80) != 0x00;
    /* LocalColorTableのサイズ */
    int localColorTableSize = (bytes[offset + 0x09] & 0x07);
    
    //サイズを決める
    size = 0x0A;
    if(localColorTableFlag){
      size += Math.pow(2, (localColorTableSize + 1)) * 3;
    }
    size += 1; //LZW Minimum Code Size
    
    //ImageData
    blockSize = bytes[offset + size] & 0xFF;
    size += 1;
    while(blockSize != 0x00){
      size += blockSize;
      blockSize = bytes[offset + size] & 0xFF;
      size += 1;
    }
    
    //バイト列のコピー
    this.bytes = Util.subarray(bytes, offset, size);
  }
  public int getImageSeparator(){
    return bytes[0] & 0xFF;
  }
  public int ImageLeftPosition(){
    return (bytes[1] & 0xFF) + ((bytes[2] & 0xFF) << 8);
  }
  public int getImageTopPosition(){
    return (bytes[3] & 0xFF) + ((bytes[4] & 0xFF) << 8);
  }
  public int getImageWidth(){
    return (bytes[5] & 0xFF) + ((bytes[6] & 0xFF) << 8);
  }
  public int getImageHeight(){
    return (bytes[7] & 0xFF) + ((bytes[8] & 0xFF) << 8);
  }
  public int getLocalColorTableFlag(){
    return (bytes[9] & 0x80) >> 7;
  }
  public int getInterlaceFlag(){
    return (bytes[9] & 0x40) >> 6;
  }
  public int getSortFlag() {
    return (bytes[9] & 0x20) >> 5;
  }
  public int getReserved() {
    return (bytes[9] & 0x18) >> 2;
  }
  public int getSizeOfLocalColorTable(){
    return bytes[9] & 0x03;
  }
  public int[] getLocalColorTable(){
    if(getLocalColorTableFlag() == 0)
      return new int[0];
    int[] colors = new int[(int)Math.pow(2, getSizeOfLocalColorTable() + 1)];
    for(int i=0;i<colors.length;i++)
      colors[i] = ((bytes[10 + (i * 3)] & 0xFF) << 16) + ((bytes[10 + (i * 3) + 1] & 0xFF) << 8) + (bytes[10 + (i * 3) + 2] & 0xFF);
    return colors;
  }
  public int getLZWMinimumCodeSize(){
    if(getLocalColorTableFlag() == 0){
      return bytes[10] & 0xFF;
    }else{
      return bytes[10 + (int)Math.pow(2, getSizeOfLocalColorTable() + 1) * 3] & 0xFF;
    }
  }
}

class ApplicationExtension {
  public byte[] bytes;
  public int size;
  public ApplicationExtension(byte[] bytes, int offset) {
    int blockSize;
    // サイズを決める
    size = 0x0E;

    blockSize = bytes[offset + size] & 0xFF;
    size += 1;
    while(blockSize != 0x00){
      size += blockSize;
      blockSize = bytes[offset + size] & 0xFF;
      size += 1;
    }
    
    //バイト列のコピー
    this.bytes = Util.subarray(bytes, offset, size);
  }
  
  public int getExtensionIntroducer(){
    return bytes[0] & 0xFF;
  }
  public int getExtensionLabel(){
    return bytes[1] & 0xFF;
  }
  public int getBlockSize1(){
    return bytes[2] & 0xFF;
  }
  public String getApplicationIdentifier(){
    return new String(bytes, 3 , 8);
  }
  public String getApplicationAuthenticationCode(){
    return new String(bytes, 11, 3);
  }
}

class GraphicControlExtension {
  public byte[] bytes;
  public int size;
  public GraphicControlExtension(byte[] bytes, int offset) {
    size = 8;
    this.bytes = Util.subarray(bytes, offset, size);
  }
  
  public int getExtensionIntroducer(){
    return bytes[0] & 0xFF;
  }
  public int getGraphicControlLabel(){
    return bytes[1] & 0xFF;
  }
  public int getBlockSize(){
    return bytes[2] & 0xFF;
  }
  public int getReserved(){
    return (bytes[3] & 0xE0) >> 5;
  }
  public int getDisposalMothod(){
    return (bytes[3] & 0x1C) >> 2;
  }
  public int getUserInputFlag(){
    return (bytes[3] & 0x02) >> 1;
  }
  public int getTransparentColorFlag(){
    return bytes[3] & 0x01;
  }
  public int getDelayTime(){
    return (bytes[4] & 0xFF) + ((bytes[5] & 0xFF) << 8);
  }
  public int getTransparentColorIndex(){
    return bytes[6];
  }
}

class CommentExtension {
  public byte[] bytes;
  public int size;
  public CommentExtension(byte[] bytes, int offset) {
    int blockSize;
    //サイズを決める
    size = 0x02;
    
    blockSize = bytes[offset + size] & 0xFF;
    size+=1;
    while(blockSize != 0x00){
      size += blockSize;
      blockSize = bytes[offset + size] & 0xFF;
      size += 1;
    }
    
    //バイト列のコピー
    this.bytes = Util.subarray(bytes, offset, size);
  }
}

class PlainTextExtension {
  public byte[] bytes;
  public int size;
  public PlainTextExtension(byte[] bytes, int offset){
    int blockSize;
    //サイズを決める
    size = 0x0F;
    
    blockSize = bytes[offset + size] & 0xFF;
    size+=1;
    while(blockSize != 0x00){
      size += blockSize;
      blockSize = bytes[offset + size] & 0xFF;
      size += 1;
    }
    
    //バイト列のコピー
    this.bytes = Util.subarray(bytes, offset, size);
  }
}

class Util {
  
  private static final boolean DUMP_FLAG = true;
  
  /**
   * バイト列をダンプ出力する
   */
  public static void dump(byte[] values, int length){
    if(!DUMP_FLAG)
      return;
    
    // 出力バッファ
    StringBuffer buffer = new StringBuffer();

    // バイト列を文字列に格納する
    for(int i=0; i<length; i++){

      //バッファに出力バイトをためる
      buffer.append(toHex(values[i], 2)).append(" ");

      // 16バイト格納ごとに出力する
      if((i + 1) % 16 == 0) 
        buffer.append("\n");
    }

    // ダンプ出力する
    System.out.println(buffer.toString());
    System.out.println();
  }
  
  /**
   * 16進数に変換してフォーマットを整えるメソッド
   */
  public static String toHex(int value , int length){
    //16進数に変換
    String hex = Integer.toHexString(value);
    // 大文字に変換
    hex = hex.toUpperCase();
    
    if(hex.length() < length){
      while(hex.length() < length)
        hex = "0" + hex;
    }else if(hex.length() > length){
      hex = hex.substring(hex.length() - length);
    }
    return hex;
  }
  
  /**
   * バイト配列の切り出しを行う
   */
  public static byte[] subarray(byte[] bytes, int offset, int size){
    byte[] ret = new byte[size];
    for(int i=0;i<ret.length;i++)
      ret[i] = bytes[offset + i];
    return ret;
  }
}

2010-06-10

Android版GIFプレーヤをGoogleCodeに公開しました。

今までブログに貼りつけていたコードですが、

GoogleCodeにてソースを公開する方法に変更しました。

android-gifview - Project Hosting on Google Code
http://code.google.com/p/android-gifview/

前バージョンとの差分は以下の通りです。

  • デコードに時間がかかると高速に再生されるバグを修正
  • 自動再生ではなく、初期状態は停止状態に変更
  • 一時停止機能を追加
  • 停止機能を追加
  • フレーム移動機能を追加

尚、テスト用のGifアニメーションを@mrshiromi さんが提供してくださいました!

ありがとうございます!!

f:id:tomorrowkey:20100610233717g:image


バグや要望を受け付けています。

なにかありましたらコメントや、メールなど気軽にどうぞ。

2010-04-29

AndroidでGIFアニメーションが動いたんだもんね(バグフィックス)

変更点

  • SizeOfLocalColorTableが0x04以上だった場合に対応

下3bitがSizeOfLocalColorTableSizeなんだけど、マスクするところで0x03としていたのがバグでした。

0000 0111は0x07ですよね…

ソース

  • GifDecoder
import java.io.InputStream;
import java.util.Vector;

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;

public class GifDecoder {
	/**
	 * File read status: No errors.
	 */
	public static final int STATUS_OK = 0;
	/**
	 * File read status: Error decoding file (may be partially decoded)
	 */
	public static final int STATUS_FORMAT_ERROR = 1;
	/**
	 * File read status: Unable to open source.
	 */
	public static final int STATUS_OPEN_ERROR = 2;
	/** max decoder pixel stack size */
	protected static final int MAX_STACK_SIZE = 4096;
	protected InputStream in;
	protected int status;
	protected int width; // full image width
	protected int height; // full image height
	protected boolean gctFlag; // global color table used
	protected int gctSize; // size of global color table
	protected int loopCount = 1; // iterations; 0 = repeat forever
	protected int[] gct; // global color table
	protected int[] lct; // local color table
	protected int[] act; // active color table
	protected int bgIndex; // background color index
	protected int bgColor; // background color
	protected int lastBgColor; // previous bg color
	protected int pixelAspect; // pixel aspect ratio
	protected boolean lctFlag; // local color table flag
	protected boolean interlace; // interlace flag
	protected int lctSize; // local color table size
	protected int ix, iy, iw, ih; // current image rectangle
	protected int lrx, lry, lrw, lrh;
	protected Bitmap image; // current frame
	protected Bitmap lastBitmap; // previous frame
	protected byte[] block = new byte[256]; // current data block
	protected int blockSize = 0; // block size last graphic control extension info
	protected int dispose = 0; // 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev
	protected int lastDispose = 0;
	protected boolean transparency = false; // use transparent color
	protected int delay = 0; // delay in milliseconds
	protected int transIndex; // transparent color index
	// LZW decoder working arrays
	protected short[] prefix;
	protected byte[] suffix;
	protected byte[] pixelStack;
	protected byte[] pixels;
	protected Vector<GifFrame> frames; // frames read from current file
	protected int frameCount;

	private static class GifFrame {
		public GifFrame(Bitmap im, int del) {
			image = im;
			delay = del;
		}

		public Bitmap image;
		public int delay;
	}

	/**
	 * Gets display duration for specified frame.
	 * 
	 * @param n
	 *          int index of frame
	 * @return delay in milliseconds
	 */
	public int getDelay(int n) {
		delay = -1;
		if ((n >= 0) && (n < frameCount)) {
			delay = frames.elementAt(n).delay;
		}
		return delay;
	}

	/**
	 * Gets the number of frames read from file.
	 * 
	 * @return frame count
	 */
	public int getFrameCount() {
		return frameCount;
	}

	/**
	 * Gets the first (or only) image read.
	 * 
	 * @return BufferedBitmap containing first frame, or null if none.
	 */
	public Bitmap getBitmap() {
		return getFrame(0);
	}

	/**
	 * Gets the "Netscape" iteration count, if any. A count of 0 means repeat indefinitiely.
	 * 
	 * @return iteration count if one was specified, else 1.
	 */
	public int getLoopCount() {
		return loopCount;
	}

	/**
	 * Creates new frame image from current data (and previous frames as specified by their disposition codes).
	 */
	protected void setPixels() {
		// expose destination image's pixels as int array
		int[] dest = new int[width * height];
		// fill in starting image contents based on last image's dispose code
		if (lastDispose > 0) {
			if (lastDispose == 3) {
				// use image before last
				int n = frameCount - 2;
				if (n > 0) {
					lastBitmap = getFrame(n - 1);
				} else {
					lastBitmap = null;
				}
			}
			if (lastBitmap != null) {
				lastBitmap.getPixels(dest, 0, width, 0, 0, width, height);
				// copy pixels
				if (lastDispose == 2) {
					// fill last image rect area with background color
					int c = 0;
					if (!transparency) {
						c = lastBgColor;
					}
					for (int i = 0; i < lrh; i++) {
						int n1 = (lry + i) * width + lrx;
						int n2 = n1 + lrw;
						for (int k = n1; k < n2; k++) {
							dest[k] = c;
						}
					}
				}
			}
		}
		// copy each source line to the appropriate place in the destination
		int pass = 1;
		int inc = 8;
		int iline = 0;
		for (int i = 0; i < ih; i++) {
			int line = i;
			if (interlace) {
				if (iline >= ih) {
					pass++;
					switch (pass) {
					case 2:
						iline = 4;
						break;
					case 3:
						iline = 2;
						inc = 4;
						break;
					case 4:
						iline = 1;
						inc = 2;
						break;
					default:
						break;
					}
				}
				line = iline;
				iline += inc;
			}
			line += iy;
			if (line < height) {
				int k = line * width;
				int dx = k + ix; // start of line in dest
				int dlim = dx + iw; // end of dest line
				if ((k + width) < dlim) {
					dlim = k + width; // past dest edge
				}
				int sx = i * iw; // start of line in source
				while (dx < dlim) {
					// map color and insert in destination
					int index = ((int) pixels[sx++]) & 0xff;
					int c = act[index];
					if (c != 0) {
						dest[dx] = c;
					}
					dx++;
				}
			}
		}
		image = Bitmap.createBitmap(dest, width, height, Config.ARGB_4444);
	}

	/**
	 * Gets the image contents of frame n.
	 * 
	 * @return BufferedBitmap representation of frame, or null if n is invalid.
	 */
	public Bitmap getFrame(int n) {
		if (frameCount <= 0)
			return null;
		n = n % frameCount;
		return ((GifFrame) frames.elementAt(n)).image;
	}

	/**
	 * Reads GIF image from stream
	 * 
	 * @param is
	 *          containing GIF file.
	 * @return read status code (0 = no errors)
	 */
	public int read(InputStream is) {
		init();
		if (is != null) {
			in = is;
			readHeader();
			if (!err()) {
				readContents();
				if (frameCount < 0) {
					status = STATUS_FORMAT_ERROR;
				}
			}
		} else {
			status = STATUS_OPEN_ERROR;
		}
		try {
			is.close();
		} catch (Exception e) {
		}
		return status;
	}

	/**
	 * Decodes LZW image data into pixel array. Adapted from John Cristy's BitmapMagick.
	 */
	protected void decodeBitmapData() {
		int nullCode = -1;
		int npix = iw * ih;
		int available, clear, code_mask, code_size, end_of_information, in_code, old_code, bits, code, count, i, datum, data_size, first, top, bi, pi;
		if ((pixels == null) || (pixels.length < npix)) {
			pixels = new byte[npix]; // allocate new pixel array
		}
		if (prefix == null) {
			prefix = new short[MAX_STACK_SIZE];
		}
		if (suffix == null) {
			suffix = new byte[MAX_STACK_SIZE];
		}
		if (pixelStack == null) {
			pixelStack = new byte[MAX_STACK_SIZE + 1];
		}
		// Initialize GIF data stream decoder.
		data_size = read();
		clear = 1 << data_size;
		end_of_information = clear + 1;
		available = clear + 2;
		old_code = nullCode;
		code_size = data_size + 1;
		code_mask = (1 << code_size) - 1;
		for (code = 0; code < clear; code++) {
			prefix[code] = 0; // XXX ArrayIndexOutOfBoundsException
			suffix[code] = (byte) code;
		}
		// Decode GIF pixel stream.
		datum = bits = count = first = top = pi = bi = 0;
		for (i = 0; i < npix;) {
			if (top == 0) {
				if (bits < code_size) {
					// Load bytes until there are enough bits for a code.
					if (count == 0) {
						// Read a new data block.
						count = readBlock();
						if (count <= 0) {
							break;
						}
						bi = 0;
					}
					datum += (((int) block[bi]) & 0xff) << bits;
					bits += 8;
					bi++;
					count--;
					continue;
				}
				// Get the next code.
				code = datum & code_mask;
				datum >>= code_size;
				bits -= code_size;
				// Interpret the code
				if ((code > available) || (code == end_of_information)) {
					break;
				}
				if (code == clear) {
					// Reset decoder.
					code_size = data_size + 1;
					code_mask = (1 << code_size) - 1;
					available = clear + 2;
					old_code = nullCode;
					continue;
				}
				if (old_code == nullCode) {
					pixelStack[top++] = suffix[code];
					old_code = code;
					first = code;
					continue;
				}
				in_code = code;
				if (code == available) {
					pixelStack[top++] = (byte) first;
					code = old_code;
				}
				while (code > clear) {
					pixelStack[top++] = suffix[code];
					code = prefix[code];
				}
				first = ((int) suffix[code]) & 0xff;
				// Add a new string to the string table,
				if (available >= MAX_STACK_SIZE) {
					break;
				}
				pixelStack[top++] = (byte) first;
				prefix[available] = (short) old_code;
				suffix[available] = (byte) first;
				available++;
				if (((available & code_mask) == 0) && (available < MAX_STACK_SIZE)) {
					code_size++;
					code_mask += available;
				}
				old_code = in_code;
			}
			// Pop a pixel off the pixel stack.
			top--;
			pixels[pi++] = pixelStack[top];
			i++;
		}
		for (i = pi; i < npix; i++) {
			pixels[i] = 0; // clear missing pixels
		}
	}

	/**
	 * Returns true if an error was encountered during reading/decoding
	 */
	protected boolean err() {
		return status != STATUS_OK;
	}

	/**
	 * Initializes or re-initializes reader
	 */
	protected void init() {
		status = STATUS_OK;
		frameCount = 0;
		frames = new Vector<GifFrame>();
		gct = null;
		lct = null;
	}

	/**
	 * Reads a single byte from the input stream.
	 */
	protected int read() {
		int curByte = 0;
		try {
			curByte = in.read();
		} catch (Exception e) {
			status = STATUS_FORMAT_ERROR;
		}
		return curByte;
	}

	/**
	 * Reads next variable length block from input.
	 * 
	 * @return number of bytes stored in "buffer"
	 */
	protected int readBlock() {
		blockSize = read();
		int n = 0;
		if (blockSize > 0) {
			try {
				int count = 0;
				while (n < blockSize) {
					count = in.read(block, n, blockSize - n);
					if (count == -1) {
						break;
					}
					n += count;
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
			if (n < blockSize) {
				status = STATUS_FORMAT_ERROR;
			}
		}
		return n;
	}

	/**
	 * Reads color table as 256 RGB integer values
	 * 
	 * @param ncolors
	 *          int number of colors to read
	 * @return int array containing 256 colors (packed ARGB with full alpha)
	 */
	protected int[] readColorTable(int ncolors) {
		int nbytes = 3 * ncolors;
		int[] tab = null;
		byte[] c = new byte[nbytes];
		int n = 0;
		try {
			n = in.read(c);
		} catch (Exception e) {
			e.printStackTrace();
		}
		if (n < nbytes) {
			status = STATUS_FORMAT_ERROR;
		} else {
			tab = new int[256]; // max size to avoid bounds checks
			int i = 0;
			int j = 0;
			while (i < ncolors) {
				int r = ((int) c[j++]) & 0xff;
				int g = ((int) c[j++]) & 0xff;
				int b = ((int) c[j++]) & 0xff;
				tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b;
			}
		}
		return tab;
	}

	/**
	 * Main file parser. Reads GIF content blocks.
	 */
	protected void readContents() {
		// read GIF file content blocks
		boolean done = false;
		while (!(done || err())) {
			int code = read();
			switch (code) {
			case 0x2C: // image separator
				readBitmap();
				break;
			case 0x21: // extension
				code = read();
				switch (code) {
				case 0xf9: // graphics control extension
					readGraphicControlExt();
					break;
				case 0xff: // application extension
					readBlock();
					String app = "";
					for (int i = 0; i < 11; i++) {
						app += (char) block[i];
					}
					if (app.equals("NETSCAPE2.0")) {
						readNetscapeExt();
					} else {
						skip(); // don't care
					}
					break;
				case 0xfe:// comment extension
					skip();
					break;
				case 0x01:// plain text extension
					skip();
					break;
				default: // uninteresting extension
					skip();
				}
				break;
			case 0x3b: // terminator
				done = true;
				break;
			case 0x00: // bad byte, but keep going and see what happens break;
			default:
				status = STATUS_FORMAT_ERROR;
			}
		}
	}

	/**
	 * Reads Graphics Control Extension values
	 */
	protected void readGraphicControlExt() {
		read(); // block size
		int packed = read(); // packed fields
		dispose = (packed & 0x1c) >> 2; // disposal method
		if (dispose == 0) {
			dispose = 1; // elect to keep old image if discretionary
		}
		transparency = (packed & 1) != 0;
		delay = readShort() * 10; // delay in milliseconds
		transIndex = read(); // transparent color index
		read(); // block terminator
	}

	/**
	 * Reads GIF file header information.
	 */
	protected void readHeader() {
		String id = "";
		for (int i = 0; i < 6; i++) {
			id += (char) read();
		}
		if (!id.startsWith("GIF")) {
			status = STATUS_FORMAT_ERROR;
			return;
		}
		readLSD();
		if (gctFlag && !err()) {
			gct = readColorTable(gctSize);
			bgColor = gct[bgIndex];
		}
	}

	/**
	 * Reads next frame image
	 */
	protected void readBitmap() {
		ix = readShort(); // (sub)image position & size
		iy = readShort();
		iw = readShort();
		ih = readShort();
		int packed = read();
		lctFlag = (packed & 0x80) != 0; // 1 - local color table flag interlace
		lctSize = (int) Math.pow(2, (packed & 0x07) + 1);
		// = (packed & 0x40) != 0; // 2 - interlace flag
		// 3 - sort flag
		// 4-5 - reserved lctSize = 2 << (packed & 7); // 6-8 - local color
		// table size
		interlace = (packed & 0x40) != 0;
		if (lctFlag) {
			lct = readColorTable(lctSize); // read table
			act = lct; // make local table active
		} else {
			act = gct; // make global table active
			if (bgIndex == transIndex) {
				bgColor = 0;
			}
		}
		int save = 0;
		if (transparency) {
			save = act[transIndex];
			act[transIndex] = 0; // set transparent color if specified
		}
		if (act == null) {
			status = STATUS_FORMAT_ERROR; // no color table defined
		}
		if (err()) {
			return;
		}
		decodeBitmapData(); // decode pixel data
		skip();
		if (err()) {
			return;
		}
		frameCount++;
		// create new image to receive frame data
		image = Bitmap.createBitmap(width, height, Config.ARGB_4444);
		setPixels(); // transfer pixel data to image
		frames.addElement(new GifFrame(image, delay)); // add image to frame
		// list
		if (transparency) {
			act[transIndex] = save;
		}
		resetFrame();
	}

	/**
	 * Reads Logical Screen Descriptor
	 */
	protected void readLSD() {
		// logical screen size
		width = readShort();
		height = readShort();
		// packed fields
		int packed = read();
		gctFlag = (packed & 0x80) != 0; // 1 : global color table flag
		// 2-4 : color resolution
		// 5 : gct sort flag
		gctSize = 2 << (packed & 7); // 6-8 : gct size
		bgIndex = read(); // background color index
		pixelAspect = read(); // pixel aspect ratio
	}

	/**
	 * Reads Netscape extenstion to obtain iteration count
	 */
	protected void readNetscapeExt() {
		do {
			readBlock();
			if (block[0] == 1) {
				// loop count sub-block
				int b1 = ((int) block[1]) & 0xff;
				int b2 = ((int) block[2]) & 0xff;
				loopCount = (b2 << 8) | b1;
			}
		} while ((blockSize > 0) && !err());
	}

	/**
	 * Reads next 16-bit value, LSB first
	 */
	protected int readShort() {
		// read 16-bit value, LSB first
		return read() | (read() << 8);
	}

	/**
	 * Resets frame state for reading next image.
	 */
	protected void resetFrame() {
		lastDispose = dispose;
		lrx = ix;
		lry = iy;
		lrw = iw;
		lrh = ih;
		lastBitmap = image;
		lastBgColor = bgColor;
		dispose = 0;
		transparency = false;
		delay = 0;
		lct = null;
	}

	/**
	 * Skips variable length blocks up to and including next zero length block.
	 */
	protected void skip() {
		do {
			readBlock();
		} while ((blockSize > 0) && !err());
	}
}
  • GifView
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout.LayoutParams;

public class GifView extends View {

	private GifDecoder decoder;
	private Bitmap bitmap;

	private int width;
	private int height;

	private long time;
	private int index;

	private boolean decoding = false;

	private int resId;
	private String filePath;

	private Handler handler = new Handler();

	public GifView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	/**
	 * Constructor
	 */
	public GifView(Context context) {
		super(context);
	}

	private InputStream getInputStream() {
		if (filePath != null)
			try {
				return new FileInputStream(filePath);
			} catch (FileNotFoundException e) {
			}
		if (resId > 0)
			return getContext().getResources().openRawResource(resId);
		return null;
	}

	/**
	 * set gif file path
	 * 
	 * @param filePath
	 */
	public void setGif(String filePath) {
		this.filePath = filePath;
		decode();
	}

	/**
	 * set gif res id
	 * 
	 * @param resId
	 */
	public void setGif(int resId) {
		this.resId = resId;
		decode();
	}

	private void decode() {
		release();
		decoding = true;
		time = System.currentTimeMillis();
		index = 0;

		new Thread() {
			@Override
			public void run() {
				try {
					GifDecoder decoder = new GifDecoder();
					decoder.read(getInputStream());
					width = decoder.width;
					height = decoder.height;
					if (width == 0 || height == 0)
						throw new Exception();
					GifView.this.decoder = decoder;
				} catch (Throwable e) {
					try {
						Bitmap bitmap = BitmapFactory.decodeStream(getInputStream());
						width = bitmap.getWidth();
						height = bitmap.getHeight();
						GifView.this.bitmap = bitmap;
					} catch (Exception e1) {
					}
				} finally {
					handler.post(new Runnable() {
						@Override
						public void run() {
							GifView.this.setLayoutParams(new LayoutParams(width, height));
						}
					});
					postInvalidate();
					decoding = false;
				}
			}
		}.start();
	}

	/*
	 * @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(width, height); }
	 */
	public void release() {
		decoder = null;
		bitmap = null;
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);

		if (decoding) {
			Paint paint = new Paint();
			paint.setColor(Color.rgb(180, 150, 150));
			canvas.drawText("Loading ...", 20, 30, paint);
			invalidate();
			return;
		}

		if (bitmap != null) {
			canvas.drawBitmap(bitmap, 0, 0, null);
			return;
		}
		if (decoder == null)
			return;
		long now = System.currentTimeMillis();
		if (time + decoder.getDelay(index) < now) {
			time += decoder.getDelay(index);
			index++;
			if (index >= decoder.getFrameCount()) {
				index = 0;
			}
		}
		Bitmap bitmap = decoder.getFrame(index);
		if (bitmap != null)
			canvas.drawBitmap(bitmap, 0, 0, null);

		invalidate();
	}
}

2010-04-26

AndroidでGIFアニメーションが動いたんだもんね(バグフィックス)

注意

バグが発見されました。

最新版はこちらです。

変更点

  • フレーム個別のDelayに対応していなかった
  • インターレースを使ったGIFが正常に表示されない
  • LocalColorTableを使ったGIFが正常に表示されない

直しました。

他にもなにか直した気がするんですけど、忘れました。

そして、クラスを二つに分けました

  • GifDecoder
  • GifView

おねがい

正常に表示されないGIFアニメーションがありましたら、メールで送付してください。

それなりに見てみます。

tomorrowkey@gmail.com

ソース

  • GifDecoder
import java.io.InputStream;
import java.util.Vector;

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;

public class GifDecoder {
	/**
	 * File read status: No errors.
	 */
	public static final int STATUS_OK = 0;
	/**
	 * File read status: Error decoding file (may be partially decoded)
	 */
	public static final int STATUS_FORMAT_ERROR = 1;
	/**
	 * File read status: Unable to open source.
	 */
	public static final int STATUS_OPEN_ERROR = 2;
	/** max decoder pixel stack size */
	protected static final int MAX_STACK_SIZE = 4096;
	protected InputStream in;
	protected int status;
	protected int width; // full image width
	protected int height; // full image height
	protected boolean gctFlag; // global color table used
	protected int gctSize; // size of global color table
	protected int loopCount = 1; // iterations; 0 = repeat forever
	protected int[] gct; // global color table
	protected int[] lct; // local color table
	protected int[] act; // active color table
	protected int bgIndex; // background color index
	protected int bgColor; // background color
	protected int lastBgColor; // previous bg color
	protected int pixelAspect; // pixel aspect ratio
	protected boolean lctFlag; // local color table flag
	protected boolean interlace; // interlace flag
	protected int lctSize; // local color table size
	protected int ix, iy, iw, ih; // current image rectangle
	protected int lrx, lry, lrw, lrh;
	protected Bitmap image; // current frame
	protected Bitmap lastBitmap; // previous frame
	protected byte[] block = new byte[256]; // current data block
	protected int blockSize = 0; // block size last graphic control extension info
	protected int dispose = 0; // 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev
	protected int lastDispose = 0;
	protected boolean transparency = false; // use transparent color
	protected int delay = 0; // delay in milliseconds
	protected int transIndex; // transparent color index
	// LZW decoder working arrays
	protected short[] prefix;
	protected byte[] suffix;
	protected byte[] pixelStack;
	protected byte[] pixels;
	protected Vector<GifFrame> frames; // frames read from current file
	protected int frameCount;

	private static class GifFrame {
		public GifFrame(Bitmap im, int del) {
			image = im;
			delay = del;
		}

		public Bitmap image;
		public int delay;
	}

	/**
	 * Gets display duration for specified frame.
	 * 
	 * @param n
	 *          int index of frame
	 * @return delay in milliseconds
	 */
	public int getDelay(int n) {
		delay = -1;
		if ((n >= 0) && (n < frameCount)) {
			delay = frames.elementAt(n).delay;
		}
		return delay;
	}

	/**
	 * Gets the number of frames read from file.
	 * 
	 * @return frame count
	 */
	public int getFrameCount() {
		return frameCount;
	}

	/**
	 * Gets the first (or only) image read.
	 * 
	 * @return BufferedBitmap containing first frame, or null if none.
	 */
	public Bitmap getBitmap() {
		return getFrame(0);
	}

	/**
	 * Gets the "Netscape" iteration count, if any. A count of 0 means repeat indefinitiely.
	 * 
	 * @return iteration count if one was specified, else 1.
	 */
	public int getLoopCount() {
		return loopCount;
	}

	/**
	 * Creates new frame image from current data (and previous frames as specified by their disposition codes).
	 */
	protected void setPixels() {
		// expose destination image's pixels as int array
		int[] dest = new int[width * height];
		// fill in starting image contents based on last image's dispose code
		if (lastDispose > 0) {
			if (lastDispose == 3) {
				// use image before last
				int n = frameCount - 2;
				if (n > 0) {
					lastBitmap = getFrame(n - 1);
				} else {
					lastBitmap = null;
				}
			}
			if (lastBitmap != null) {
				lastBitmap.getPixels(dest, 0, width, 0, 0, width, height);
				// copy pixels
				if (lastDispose == 2) {
					// fill last image rect area with background color
					int c = 0;
					if (!transparency) {
						c = lastBgColor;
					}
					for (int i = 0; i < lrh; i++) {
						int n1 = (lry + i) * width + lrx;
						int n2 = n1 + lrw;
						for (int k = n1; k < n2; k++) {
							dest[k] = c;
						}
					}
				}
			}
		}
		// copy each source line to the appropriate place in the destination
		int pass = 1;
		int inc = 8;
		int iline = 0;
		for (int i = 0; i < ih; i++) {
			int line = i;
			if (interlace) {
				if (iline >= ih) {
					pass++;
					switch (pass) {
					case 2:
						iline = 4;
						break;
					case 3:
						iline = 2;
						inc = 4;
						break;
					case 4:
						iline = 1;
						inc = 2;
						break;
					default:
						break;
					}
				}
				line = iline;
				iline += inc;
			}
			line += iy;
			if (line < height) {
				int k = line * width;
				int dx = k + ix; // start of line in dest
				int dlim = dx + iw; // end of dest line
				if ((k + width) < dlim) {
					dlim = k + width; // past dest edge
				}
				int sx = i * iw; // start of line in source
				while (dx < dlim) {
					// map color and insert in destination
					int index = ((int) pixels[sx++]) & 0xff;
					int c = act[index];
					if (c != 0) {
						dest[dx] = c;
					}
					dx++;
				}
			}
		}
		image = Bitmap.createBitmap(dest, width, height, Config.ARGB_4444);
	}

	/**
	 * Gets the image contents of frame n.
	 * 
	 * @return BufferedBitmap representation of frame, or null if n is invalid.
	 */
	public Bitmap getFrame(int n) {
		if (frameCount <= 0)
			return null;
		n = n % frameCount;
		return ((GifFrame) frames.elementAt(n)).image;
	}

	/**
	 * Reads GIF image from stream
	 * 
	 * @param is
	 *          containing GIF file.
	 * @return read status code (0 = no errors)
	 */
	public int read(InputStream is) {
		init();
		if (is != null) {
			in = is;
			readHeader();
			if (!err()) {
				readContents();
				if (frameCount < 0) {
					status = STATUS_FORMAT_ERROR;
				}
			}
		} else {
			status = STATUS_OPEN_ERROR;
		}
		try {
			is.close();
		} catch (Exception e) {
		}
		return status;
	}

	/**
	 * Decodes LZW image data into pixel array. Adapted from John Cristy's BitmapMagick.
	 */
	protected void decodeBitmapData() {
		int nullCode = -1;
		int npix = iw * ih;
		int available, clear, code_mask, code_size, end_of_information, in_code, old_code, bits, code, count, i, datum, data_size, first, top, bi, pi;
		if ((pixels == null) || (pixels.length < npix)) {
			pixels = new byte[npix]; // allocate new pixel array
		}
		if (prefix == null) {
			prefix = new short[MAX_STACK_SIZE];
		}
		if (suffix == null) {
			suffix = new byte[MAX_STACK_SIZE];
		}
		if (pixelStack == null) {
			pixelStack = new byte[MAX_STACK_SIZE + 1];
		}
		// Initialize GIF data stream decoder.
		data_size = read();
		clear = 1 << data_size;
		end_of_information = clear + 1;
		available = clear + 2;
		old_code = nullCode;
		code_size = data_size + 1;
		code_mask = (1 << code_size) - 1;
		for (code = 0; code < clear; code++) {
			prefix[code] = 0; // XXX ArrayIndexOutOfBoundsException
			suffix[code] = (byte) code;
		}
		// Decode GIF pixel stream.
		datum = bits = count = first = top = pi = bi = 0;
		for (i = 0; i < npix;) {
			if (top == 0) {
				if (bits < code_size) {
					// Load bytes until there are enough bits for a code.
					if (count == 0) {
						// Read a new data block.
						count = readBlock();
						if (count <= 0) {
							break;
						}
						bi = 0;
					}
					datum += (((int) block[bi]) & 0xff) << bits;
					bits += 8;
					bi++;
					count--;
					continue;
				}
				// Get the next code.
				code = datum & code_mask;
				datum >>= code_size;
				bits -= code_size;
				// Interpret the code
				if ((code > available) || (code == end_of_information)) {
					break;
				}
				if (code == clear) {
					// Reset decoder.
					code_size = data_size + 1;
					code_mask = (1 << code_size) - 1;
					available = clear + 2;
					old_code = nullCode;
					continue;
				}
				if (old_code == nullCode) {
					pixelStack[top++] = suffix[code];
					old_code = code;
					first = code;
					continue;
				}
				in_code = code;
				if (code == available) {
					pixelStack[top++] = (byte) first;
					code = old_code;
				}
				while (code > clear) {
					pixelStack[top++] = suffix[code];
					code = prefix[code];
				}
				first = ((int) suffix[code]) & 0xff;
				// Add a new string to the string table,
				if (available >= MAX_STACK_SIZE) {
					break;
				}
				pixelStack[top++] = (byte) first;
				prefix[available] = (short) old_code;
				suffix[available] = (byte) first;
				available++;
				if (((available & code_mask) == 0) && (available < MAX_STACK_SIZE)) {
					code_size++;
					code_mask += available;
				}
				old_code = in_code;
			}
			// Pop a pixel off the pixel stack.
			top--;
			pixels[pi++] = pixelStack[top];
			i++;
		}
		for (i = pi; i < npix; i++) {
			pixels[i] = 0; // clear missing pixels
		}
	}

	/**
	 * Returns true if an error was encountered during reading/decoding
	 */
	protected boolean err() {
		return status != STATUS_OK;
	}

	/**
	 * Initializes or re-initializes reader
	 */
	protected void init() {
		status = STATUS_OK;
		frameCount = 0;
		frames = new Vector<GifFrame>();
		gct = null;
		lct = null;
	}

	/**
	 * Reads a single byte from the input stream.
	 */
	protected int read() {
		int curByte = 0;
		try {
			curByte = in.read();
		} catch (Exception e) {
			status = STATUS_FORMAT_ERROR;
		}
		return curByte;
	}

	/**
	 * Reads next variable length block from input.
	 * 
	 * @return number of bytes stored in "buffer"
	 */
	protected int readBlock() {
		blockSize = read();
		int n = 0;
		if (blockSize > 0) {
			try {
				int count = 0;
				while (n < blockSize) {
					count = in.read(block, n, blockSize - n);
					if (count == -1) {
						break;
					}
					n += count;
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
			if (n < blockSize) {
				status = STATUS_FORMAT_ERROR;
			}
		}
		return n;
	}

	/**
	 * Reads color table as 256 RGB integer values
	 * 
	 * @param ncolors
	 *          int number of colors to read
	 * @return int array containing 256 colors (packed ARGB with full alpha)
	 */
	protected int[] readColorTable(int ncolors) {
		int nbytes = 3 * ncolors;
		int[] tab = null;
		byte[] c = new byte[nbytes];
		int n = 0;
		try {
			n = in.read(c);
		} catch (Exception e) {
			e.printStackTrace();
		}
		if (n < nbytes) {
			status = STATUS_FORMAT_ERROR;
		} else {
			tab = new int[256]; // max size to avoid bounds checks
			int i = 0;
			int j = 0;
			while (i < ncolors) {
				int r = ((int) c[j++]) & 0xff;
				int g = ((int) c[j++]) & 0xff;
				int b = ((int) c[j++]) & 0xff;
				tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b;
			}
		}
		return tab;
	}

	/**
	 * Main file parser. Reads GIF content blocks.
	 */
	protected void readContents() {
		// read GIF file content blocks
		boolean done = false;
		while (!(done || err())) {
			int code = read();
			switch (code) {
			case 0x2C: // image separator
				readBitmap();
				break;
			case 0x21: // extension
				code = read();
				switch (code) {
				case 0xf9: // graphics control extension
					readGraphicControlExt();
					break;
				case 0xff: // application extension
					readBlock();
					String app = "";
					for (int i = 0; i < 11; i++) {
						app += (char) block[i];
					}
					if (app.equals("NETSCAPE2.0")) {
						readNetscapeExt();
					} else {
						skip(); // don't care
					}
					break;
				case 0xfe:// comment extension
					skip();
					break;
				case 0x01:// plain text extension
					skip();
					break;
				default: // uninteresting extension
					skip();
				}
				break;
			case 0x3b: // terminator
				done = true;
				break;
			case 0x00: // bad byte, but keep going and see what happens break;
			default:
				status = STATUS_FORMAT_ERROR;
			}
		}
	}

	/**
	 * Reads Graphics Control Extension values
	 */
	protected void readGraphicControlExt() {
		read(); // block size
		int packed = read(); // packed fields
		dispose = (packed & 0x1c) >> 2; // disposal method
		if (dispose == 0) {
			dispose = 1; // elect to keep old image if discretionary
		}
		transparency = (packed & 1) != 0;
		delay = readShort() * 10; // delay in milliseconds
		transIndex = read(); // transparent color index
		read(); // block terminator
	}

	/**
	 * Reads GIF file header information.
	 */
	protected void readHeader() {
		String id = "";
		for (int i = 0; i < 6; i++) {
			id += (char) read();
		}
		if (!id.startsWith("GIF")) {
			status = STATUS_FORMAT_ERROR;
			return;
		}
		readLSD();
		if (gctFlag && !err()) {
			gct = readColorTable(gctSize);
			bgColor = gct[bgIndex];
		}
	}

	/**
	 * Reads next frame image
	 */
	protected void readBitmap() {
		ix = readShort(); // (sub)image position & size
		iy = readShort();
		iw = readShort();
		ih = readShort();
		int packed = read();
		lctFlag = (packed & 0x80) != 0; // 1 - local color table flag interlace
		lctSize = (int) Math.pow(2, (packed & 0x03) + 1);
		// = (packed & 0x40) != 0; // 2 - interlace flag
		// 3 - sort flag
		// 4-5 - reserved lctSize = 2 << (packed & 7); // 6-8 - local color
		// table size
		interlace = (packed & 0x40) != 0;
		if (lctFlag) {
			lct = readColorTable(lctSize); // read table
			act = lct; // make local table active
		} else {
			act = gct; // make global table active
			if (bgIndex == transIndex) {
				bgColor = 0;
			}
		}
		int save = 0;
		if (transparency) {
			save = act[transIndex];
			act[transIndex] = 0; // set transparent color if specified
		}
		if (act == null) {
			status = STATUS_FORMAT_ERROR; // no color table defined
		}
		if (err()) {
			return;
		}
		decodeBitmapData(); // decode pixel data
		skip();
		if (err()) {
			return;
		}
		frameCount++;
		// create new image to receive frame data
		image = Bitmap.createBitmap(width, height, Config.ARGB_4444);
		setPixels(); // transfer pixel data to image
		frames.addElement(new GifFrame(image, delay)); // add image to frame
		// list
		if (transparency) {
			act[transIndex] = save;
		}
		resetFrame();
	}

	/**
	 * Reads Logical Screen Descriptor
	 */
	protected void readLSD() {
		// logical screen size
		width = readShort();
		height = readShort();
		// packed fields
		int packed = read();
		gctFlag = (packed & 0x80) != 0; // 1 : global color table flag
		// 2-4 : color resolution
		// 5 : gct sort flag
		gctSize = 2 << (packed & 7); // 6-8 : gct size
		bgIndex = read(); // background color index
		pixelAspect = read(); // pixel aspect ratio
	}

	/**
	 * Reads Netscape extenstion to obtain iteration count
	 */
	protected void readNetscapeExt() {
		do {
			readBlock();
			if (block[0] == 1) {
				// loop count sub-block
				int b1 = ((int) block[1]) & 0xff;
				int b2 = ((int) block[2]) & 0xff;
				loopCount = (b2 << 8) | b1;
			}
		} while ((blockSize > 0) && !err());
	}

	/**
	 * Reads next 16-bit value, LSB first
	 */
	protected int readShort() {
		// read 16-bit value, LSB first
		return read() | (read() << 8);
	}

	/**
	 * Resets frame state for reading next image.
	 */
	protected void resetFrame() {
		lastDispose = dispose;
		lrx = ix;
		lry = iy;
		lrw = iw;
		lrh = ih;
		lastBitmap = image;
		lastBgColor = bgColor;
		dispose = 0;
		transparency = false;
		delay = 0;
		lct = null;
	}

	/**
	 * Skips variable length blocks up to and including next zero length block.
	 */
	protected void skip() {
		do {
			readBlock();
		} while ((blockSize > 0) && !err());
	}
}
  • GifView
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout.LayoutParams;

public class GifView extends View {

	private GifDecoder decoder;
	private Bitmap bitmap;

	private int width;
	private int height;

	private long time;
	private int index;

	private boolean decoding = false;

	private int resId;
	private String filePath;

	private Handler handler = new Handler();

	public GifView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	/**
	 * Constructor
	 */
	public GifView(Context context) {
		super(context);
	}

	private InputStream getInputStream() {
		if (filePath != null)
			try {
				return new FileInputStream(filePath);
			} catch (FileNotFoundException e) {
			}
		if (resId > 0)
			return getContext().getResources().openRawResource(resId);
		return null;
	}

	/**
	 * set gif file path
	 * 
	 * @param filePath
	 */
	public void setGif(String filePath) {
		this.filePath = filePath;
		decode();
	}

	/**
	 * set gif res id
	 * 
	 * @param resId
	 */
	public void setGif(int resId) {
		this.resId = resId;
		decode();
	}

	private void decode() {
		release();
		decoding = true;
		time = System.currentTimeMillis();
		index = 0;

		new Thread() {
			@Override
			public void run() {
				try {
					GifDecoder decoder = new GifDecoder();
					decoder.read(getInputStream());
					width = decoder.width;
					height = decoder.height;
					if (width == 0 || height == 0)
						throw new Exception();
					GifView.this.decoder = decoder;
				} catch (Throwable e) {
					try {
						Bitmap bitmap = BitmapFactory.decodeStream(getInputStream());
						width = bitmap.getWidth();
						height = bitmap.getHeight();
						GifView.this.bitmap = bitmap;
					} catch (Exception e1) {
					}
				} finally {
					handler.post(new Runnable() {
						@Override
						public void run() {
							GifView.this.setLayoutParams(new LayoutParams(width, height));
						}
					});
					postInvalidate();
					decoding = false;
				}
			}
		}.start();
	}

	/*
	 * @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(width, height); }
	 */
	public void release() {
		decoder = null;
		bitmap = null;
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);

		if (decoding) {
			Paint paint = new Paint();
			paint.setColor(Color.rgb(180, 150, 150));
			canvas.drawText("Loading ...", 20, 30, paint);
			invalidate();
			return;
		}

		if (bitmap != null) {
			canvas.drawBitmap(bitmap, 0, 0, null);
			return;
		}
		if (decoder == null)
			return;
		long now = System.currentTimeMillis();
		if (time + decoder.getDelay(index) < now) {
			time += decoder.getDelay(index);
			index++;
			if (index >= decoder.getFrameCount()) {
				index = 0;
			}
		}
		Bitmap bitmap = decoder.getFrame(index);
		if (bitmap != null)
			canvas.drawBitmap(bitmap, 0, 0, null);

		invalidate();
	}
}

2010-04-22

AndroidでGIFアニメーションが動いたんだもんね!!

ついにっ

動きました!

GIFアニメーションがっ!

追記 2010-04-22 21:06

ごく一部に正常に表示されないgifアニメーションが存在することが分かりました。

原因はまだ分かっていません。

GWあたりにでも調べてみます。

追記 2010-04-22 21:41

原因が分かりました。

gifをバラバラにする部分にバグがあります。

修正したバージョンは後日公開します。

追記 2010-04-26 16:41

こちらをご利用ください

AndroidでGIFアニメーションが動いたんだもんね(バグフィックス) - 明日の鍵

言い訳とか

以前やった方法はMovieクラスに任せてGifアニメーションをバラバラにしていたんですが

offsetが指定されているGifアニメーションだと正常に表示されないバグがありました。

じゃぁ、自分でバイナリ読んでバラバラにしてImageViewに順番に表示すればいいんじゃね?って思ったので

自分で書いてたのですが、作ったプログラムだとどうも効率が悪かったのでインターネットで探してたんです

Gifをバラバラにするプログラムなんて腐るほどあったので、いろいろ見てたら、

あるじゃないですか!Android版が!

で、よく読んでみるとそのままでGifアニメーションの表示までしてくれそーなんで

動かしてみたら動きました。

以上です。

URL

GifView.java - trunk/Explorer/src/jude/android/explorer/view - Code Search

すこしだけ変更加えました

オリジナルのプログラムだと、まずMovieクラスを使って表示しようとしているので

Movieクラスのくだりを全部消しました。

動作

大きい画像だと重たいです。

GIFアニメーションを自分で用意して、それを使うのであればMovieクラスを使った方が軽いです。ネイティブですから。

デコ美の様にストレージ内のGIFアニメーションを動かすのであれば、重たいけどこのプログラム使った方がいいです。

ソース

GifView.java

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.Vector;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Bitmap.Config;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

public class GifView extends View {

  private static final int DEFAULT_INTERVAL = 250;

  private GifDecoder decoder;
  private Bitmap bitmap;

  private int width;
  private int height;

  private long startTime;
  private int interval;

  private boolean decoding = false;

  private int resId;
  private String filePath;

  private GifListener listener;

  public GifView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
  }

  /**
   * Constructor
   */
  public GifView(Context context) {
    super(context);
    init();
  }

  private void init() {
    interval = DEFAULT_INTERVAL;
  }

  public void setListener(GifListener listener) {
    this.listener = listener;
  }

  private InputStream getInputStream() {
    if (filePath != null)
      try {
        return new FileInputStream(filePath);
      } catch (FileNotFoundException e) {
      }
    if (resId > 0)
      return getContext().getResources().openRawResource(resId);
    return null;
  }

  /**
   * set gif file path
   * 
   * @param filePath
   */
  public void setGif(String filePath) {
    this.filePath = filePath;
    decode();
  }

  /**
   * set gif res id
   * 
   * @param resId
   */
  public void setGif(int resId) {
    this.resId = resId;
    decode();
  }

  private void decode() {
    release();
    decoding = true;
    if (listener != null)
      listener.statusChanged(GifListener.START_DECODE);
    new Thread() {
      @Override
      public void run() {
        try {
          GifDecoder decoder = new GifDecoder();
          decoder.read(getInputStream());
          width = decoder.width;
          height = decoder.height;
          if (width == 0 || height == 0)
            throw new Exception();
          GifView.this.decoder = decoder;
          if (listener != null)
            listener.statusChanged(GifListener.DECODE_SUCCESS);
        } catch (Throwable e) {
          Log.e("GifView", e.getMessage(), e);
          try {
            Bitmap bitmap = BitmapFactory.decodeStream(getInputStream());
            width = bitmap.getWidth();
            height = bitmap.getHeight();
            GifView.this.bitmap = bitmap;
          } catch (Exception e1) {
          }
          if (listener != null)
            listener.statusChanged(GifListener.DECODE_ERROR);
        } finally {
          postInvalidate();
          decoding = false;
        }
      }
    }.start();
  }

  /**
   * set gif frame interval
   * 
   * @param interval
   */
  public void setInterval(int interval) {
    if (interval >= 20 && interval <= 10000)
      this.interval = interval;
  }

  /*
   * @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(width, height); }
   */
  public void release() {
    decoder = null;
    bitmap = null;
  }

  @Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (decoding) {
      Paint paint = new Paint();
      paint.setColor(Color.rgb(180, 150, 150));
      canvas.drawText("Loading ...", 20, 30, paint);
      invalidate();
      return;
    }
    float left = (getWidth() - width) / 2f, top = (getHeight() - height) / 2f;

    if (bitmap != null) {
      canvas.drawBitmap(bitmap, left, top, null);
      return;
    }
    long now = System.currentTimeMillis();
    if (startTime == 0)
      startTime = now;
    int time = (int) (now - startTime);

    if (decoder == null)
      return;
    int index = time / interval;
    Bitmap bitmap = decoder.getFrame(index);
    if (bitmap != null)
      canvas.drawBitmap(bitmap, left, top, null);
    if (index == decoder.getFrameCount() && listener != null)
      listener.statusChanged(GifListener.PLAY_END);

    invalidate();
  }

  public interface GifListener {
    public static final int START_DECODE = 1;
    public static final int DECODE_SUCCESS = 2;
    public static final int DECODE_ERROR = 3;
    public static final int PLAY_END = 4;

    void statusChanged(int status);
  }

  public static class GifDecoder {
    /**
     * File read status: No errors.
     */
    public static final int STATUS_OK = 0;
    /**
     * File read status: Error decoding file (may be partially decoded)
     */
    public static final int STATUS_FORMAT_ERROR = 1;
    /**
     * File read status: Unable to open source.
     */
    public static final int STATUS_OPEN_ERROR = 2;
    /** max decoder pixel stack size */
    protected static final int MAX_STACK_SIZE = 4096;
    protected InputStream in;
    protected int status;
    protected int width; // full image width
    protected int height; // full image height
    protected boolean gctFlag; // global color table used
    protected int gctSize; // size of global color table
    protected int loopCount = 1; // iterations; 0 = repeat forever
    protected int[] gct; // global color table
    protected int[] lct; // local color table
    protected int[] act; // active color table
    protected int bgIndex; // background color index
    protected int bgColor; // background color
    protected int lastBgColor; // previous bg color
    protected int pixelAspect; // pixel aspect ratio
    protected boolean lctFlag; // local color table flag
    protected boolean interlace; // interlace flag
    protected int lctSize; // local color table size
    protected int ix, iy, iw, ih; // current image rectangle
    protected int lrx, lry, lrw, lrh;
    protected Bitmap image; // current frame
    protected Bitmap lastBitmap; // previous frame
    protected byte[] block = new byte[256]; // current data block
    protected int blockSize = 0; // block size last graphic control extension info
    protected int dispose = 0; // 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev
    protected int lastDispose = 0;
    protected boolean transparency = false; // use transparent color
    protected int delay = 0; // delay in milliseconds
    protected int transIndex; // transparent color index
    // LZW decoder working arrays
    protected short[] prefix;
    protected byte[] suffix;
    protected byte[] pixelStack;
    protected byte[] pixels;
    protected Vector<GifFrame> frames; // frames read from current file
    protected int frameCount;

    private static class GifFrame {
      public GifFrame(Bitmap im, int del) {
        image = im;
        delay = del;
      }

      public Bitmap image;
      public int delay;
    }

    /**
     * Gets display duration for specified frame.
     * 
     * @param n
     *          int index of frame
     * @return delay in milliseconds
     */
    public int getDelay(int n) {
      delay = -1;
      if ((n >= 0) && (n < frameCount)) {
        delay = frames.elementAt(n).delay;
      }
      return delay;
    }

    /**
     * Gets the number of frames read from file.
     * 
     * @return frame count
     */
    public int getFrameCount() {
      return frameCount;
    }

    /**
     * Gets the first (or only) image read.
     * 
     * @return BufferedBitmap containing first frame, or null if none.
     */
    public Bitmap getBitmap() {
      return getFrame(0);
    }

    /**
     * Gets the "Netscape" iteration count, if any. A count of 0 means repeat indefinitiely.
     * 
     * @return iteration count if one was specified, else 1.
     */
    public int getLoopCount() {
      return loopCount;
    }

    /**
     * Creates new frame image from current data (and previous frames as specified by their disposition codes).
     */
    protected void setPixels() {
      // expose destination image's pixels as int array
      int[] dest = new int[width * height];
      // fill in starting image contents based on last image's dispose code
      if (lastDispose > 0) {
        if (lastDispose == 3) {
          // use image before last
          int n = frameCount - 2;
          if (n > 0) {
            lastBitmap = getFrame(n - 1);
          } else {
            lastBitmap = null;
          }
        }
        if (lastBitmap != null) {
          lastBitmap.getPixels(dest, 0, width, 0, 0, width, height);
          // copy pixels
          if (lastDispose == 2) {
            // fill last image rect area with background color
            int c = 0;
            if (!transparency) {
              c = lastBgColor;
            }
            for (int i = 0; i < lrh; i++) {
              int n1 = (lry + i) * width + lrx;
              int n2 = n1 + lrw;
              for (int k = n1; k < n2; k++) {
                dest[k] = c;
              }
            }
          }
        }
      }
      // copy each source line to the appropriate place in the destination
      int pass = 1;
      int inc = 8;
      int iline = 0;
      for (int i = 0; i < ih; i++) {
        int line = i;
        if (interlace) {
          if (iline >= ih) {
            pass++;
            switch (pass) {
            case 2:
              iline = 4;
              break;
            case 3:
              iline = 2;
              inc = 4;
              break;
            case 4:
              iline = 1;
              inc = 2;
              break;
            default:
              break;
            }
          }
          line = iline;
          iline += inc;
        }
        line += iy;
        if (line < height) {
          int k = line * width;
          int dx = k + ix; // start of line in dest
          int dlim = dx + iw; // end of dest line
          if ((k + width) < dlim) {
            dlim = k + width; // past dest edge
          }
          int sx = i * iw; // start of line in source
          while (dx < dlim) {
            // map color and insert in destination
            int index = ((int) pixels[sx++]) & 0xff;
            int c = act[index];
            if (c != 0) {
              dest[dx] = c;
            }
            dx++;
          }
        }
      }
      image = Bitmap.createBitmap(dest, width, height, Config.ARGB_4444);
    }

    /**
     * Gets the image contents of frame n.
     * 
     * @return BufferedBitmap representation of frame, or null if n is invalid.
     */
    public Bitmap getFrame(int n) {
      if (frameCount <= 0)
        return null;
      n = n % frameCount;
      return ((GifFrame) frames.elementAt(n)).image;
    }

    /**
     * Reads GIF image from stream
     * 
     * @param is
     *          containing GIF file.
     * @return read status code (0 = no errors)
     */
    public int read(InputStream is) {
      init();
      if (is != null) {
        in = is;
        readHeader();
        if (!err()) {
          readContents();
          if (frameCount < 0) {
            status = STATUS_FORMAT_ERROR;
          }
        }
      } else {
        status = STATUS_OPEN_ERROR;
      }
      try {
        is.close();
      } catch (Exception e) {
      }
      return status;
    }

    /**
     * Decodes LZW image data into pixel array. Adapted from John Cristy's BitmapMagick.
     */
    protected void decodeBitmapData() {
      int nullCode = -1;
      int npix = iw * ih;
      int available, clear, code_mask, code_size, end_of_information, in_code, old_code, bits, code, count, i, datum, data_size, first, top, bi, pi;
      if ((pixels == null) || (pixels.length < npix)) {
        pixels = new byte[npix]; // allocate new pixel array
      }
      if (prefix == null) {
        prefix = new short[MAX_STACK_SIZE];
      }
      if (suffix == null) {
        suffix = new byte[MAX_STACK_SIZE];
      }
      if (pixelStack == null) {
        pixelStack = new byte[MAX_STACK_SIZE + 1];
      }
      // Initialize GIF data stream decoder.
      data_size = read();
      clear = 1 << data_size;
      end_of_information = clear + 1;
      available = clear + 2;
      old_code = nullCode;
      code_size = data_size + 1;
      code_mask = (1 << code_size) - 1;
      for (code = 0; code < clear; code++) {
        prefix[code] = 0; // XXX ArrayIndexOutOfBoundsException
        suffix[code] = (byte) code;
      }
      // Decode GIF pixel stream.
      datum = bits = count = first = top = pi = bi = 0;
      for (i = 0; i < npix;) {
        if (top == 0) {
          if (bits < code_size) {
            // Load bytes until there are enough bits for a code.
            if (count == 0) {
              // Read a new data block.
              count = readBlock();
              if (count <= 0) {
                break;
              }
              bi = 0;
            }
            datum += (((int) block[bi]) & 0xff) << bits;
            bits += 8;
            bi++;
            count--;
            continue;
          }
          // Get the next code.
          code = datum & code_mask;
          datum >>= code_size;
          bits -= code_size;
          // Interpret the code
          if ((code > available) || (code == end_of_information)) {
            break;
          }
          if (code == clear) {
            // Reset decoder.
            code_size = data_size + 1;
            code_mask = (1 << code_size) - 1;
            available = clear + 2;
            old_code = nullCode;
            continue;
          }
          if (old_code == nullCode) {
            pixelStack[top++] = suffix[code];
            old_code = code;
            first = code;
            continue;
          }
          in_code = code;
          if (code == available) {
            pixelStack[top++] = (byte) first;
            code = old_code;
          }
          while (code > clear) {
            pixelStack[top++] = suffix[code];
            code = prefix[code];
          }
          first = ((int) suffix[code]) & 0xff;
          // Add a new string to the string table,
          if (available >= MAX_STACK_SIZE) {
            break;
          }
          pixelStack[top++] = (byte) first;
          prefix[available] = (short) old_code;
          suffix[available] = (byte) first;
          available++;
          if (((available & code_mask) == 0) && (available < MAX_STACK_SIZE)) {
            code_size++;
            code_mask += available;
          }
          old_code = in_code;
        }
        // Pop a pixel off the pixel stack.
        top--;
        pixels[pi++] = pixelStack[top];
        i++;
      }
      for (i = pi; i < npix; i++) {
        pixels[i] = 0; // clear missing pixels
      }
    }

    /**
     * Returns true if an error was encountered during reading/decoding
     */
    protected boolean err() {
      return status != STATUS_OK;
    }

    /**
     * Initializes or re-initializes reader
     */
    protected void init() {
      status = STATUS_OK;
      frameCount = 0;
      frames = new Vector<GifFrame>();
      gct = null;
      lct = null;
    }

    /**
     * Reads a single byte from the input stream.
     */
    protected int read() {
      int curByte = 0;
      try {
        curByte = in.read();
      } catch (Exception e) {
        status = STATUS_FORMAT_ERROR;
      }
      return curByte;
    }

    /**
     * Reads next variable length block from input.
     * 
     * @return number of bytes stored in "buffer"
     */
    protected int readBlock() {
      blockSize = read();
      int n = 0;
      if (blockSize > 0) {
        try {
          int count = 0;
          while (n < blockSize) {
            count = in.read(block, n, blockSize - n);
            if (count == -1) {
              break;
            }
            n += count;
          }
        } catch (Exception e) {
          e.printStackTrace();
        }
        if (n < blockSize) {
          status = STATUS_FORMAT_ERROR;
        }
      }
      return n;
    }

    /**
     * Reads color table as 256 RGB integer values
     * 
     * @param ncolors
     *          int number of colors to read
     * @return int array containing 256 colors (packed ARGB with full alpha)
     */
    protected int[] readColorTable(int ncolors) {
      int nbytes = 3 * ncolors;
      int[] tab = null;
      byte[] c = new byte[nbytes];
      int n = 0;
      try {
        n = in.read(c);
      } catch (Exception e) {
        e.printStackTrace();
      }
      if (n < nbytes) {
        status = STATUS_FORMAT_ERROR;
      } else {
        tab = new int[256]; // max size to avoid bounds checks
        int i = 0;
        int j = 0;
        while (i < ncolors) {
          int r = ((int) c[j++]) & 0xff;
          int g = ((int) c[j++]) & 0xff;
          int b = ((int) c[j++]) & 0xff;
          tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b;
        }
      }
      return tab;
    }

    /**
     * Main file parser. Reads GIF content blocks.
     */
    protected void readContents() {
      // read GIF file content blocks
      boolean done = false;
      while (!(done || err())) {
        int code = read();
        switch (code) {
        case 0x2C: // image separator
          readBitmap();
          break;
        case 0x21: // extension
          code = read();
          switch (code) {
          case 0xf9: // graphics control extension
            readGraphicControlExt();
            break;
          case 0xff: // application extension
            readBlock();
            String app = "";
            for (int i = 0; i < 11; i++) {
              app += (char) block[i];
            }
            if (app.equals("NETSCAPE2.0")) {
              readNetscapeExt();
            } else {
              skip(); // don't care
            }
            break;
          default: // uninteresting extension
            skip();
          }
          break;
        case 0x3b: // terminator
          done = true;
          break;
        case 0x00: // bad byte, but keep going and see what happens break;
        default:
          status = STATUS_FORMAT_ERROR;
        }
      }
    }

    /**
     * Reads Graphics Control Extension values
     */
    protected void readGraphicControlExt() {
      read(); // block size
      int packed = read(); // packed fields
      dispose = (packed & 0x1c) >> 2; // disposal method
      if (dispose == 0) {
        dispose = 1; // elect to keep old image if discretionary
      }
      transparency = (packed & 1) != 0;
      delay = readShort() * 10; // delay in milliseconds
      transIndex = read(); // transparent color index
      read(); // block terminator
    }

    /**
     * Reads GIF file header information.
     */
    protected void readHeader() {
      String id = "";
      for (int i = 0; i < 6; i++) {
        id += (char) read();
      }
      if (!id.startsWith("GIF")) {
        status = STATUS_FORMAT_ERROR;
        return;
      }
      readLSD();
      if (gctFlag && !err()) {
        gct = readColorTable(gctSize);
        bgColor = gct[bgIndex];
      }
    }

    /**
     * Reads next frame image
     */
    protected void readBitmap() {
      ix = readShort(); // (sub)image position & size
      iy = readShort();
      iw = readShort();
      ih = readShort();
      int packed = read();
      lctFlag = (packed & 0x80) != 0; // 1 - local color table flag interlace
      // = (packed & 0x40) != 0; // 2 -
      // interlace flag
      // 3 - sort flag
      // 4-5 - reserved lctSize = 2 << (packed & 7); // 6-8 - local color
      // table size
      if (lctFlag) {
        lct = readColorTable(lctSize); // read table
        act = lct; // make local table active
      } else {
        act = gct; // make global table active
        if (bgIndex == transIndex) {
          bgColor = 0;
        }
      }
      int save = 0;
      if (transparency) {
        save = act[transIndex];
        act[transIndex] = 0; // set transparent color if specified
      }
      if (act == null) {
        status = STATUS_FORMAT_ERROR; // no color table defined
      }
      if (err()) {
        return;
      }
      decodeBitmapData(); // decode pixel data
      skip();
      if (err()) {
        return;
      }
      frameCount++;
      // create new image to receive frame data
      image = Bitmap.createBitmap(width, height, Config.ARGB_4444);
      setPixels(); // transfer pixel data to image
      frames.addElement(new GifFrame(image, delay)); // add image to frame
      // list
      if (transparency) {
        act[transIndex] = save;
      }
      resetFrame();
    }

    /**
     * Reads Logical Screen Descriptor
     */
    protected void readLSD() {
      // logical screen size
      width = readShort();
      height = readShort();
      // packed fields
      int packed = read();
      gctFlag = (packed & 0x80) != 0; // 1 : global color table flag
      // 2-4 : color resolution
      // 5 : gct sort flag
      gctSize = 2 << (packed & 7); // 6-8 : gct size
      bgIndex = read(); // background color index
      pixelAspect = read(); // pixel aspect ratio
    }

    /**
     * Reads Netscape extenstion to obtain iteration count
     */
    protected void readNetscapeExt() {
      do {
        readBlock();
        if (block[0] == 1) {
          // loop count sub-block
          int b1 = ((int) block[1]) & 0xff;
          int b2 = ((int) block[2]) & 0xff;
          loopCount = (b2 << 8) | b1;
        }
      } while ((blockSize > 0) && !err());
    }

    /**
     * Reads next 16-bit value, LSB first
     */
    protected int readShort() {
      // read 16-bit value, LSB first
      return read() | (read() << 8);
    }

    /**
     * Resets frame state for reading next image.
     */
    protected void resetFrame() {
      lastDispose = dispose;
      lrx = ix;
      lry = iy;
      lrw = iw;
      lrh = ih;
      lastBitmap = image;
      lastBgColor = bgColor;
      dispose = 0;
      transparency = false;
      delay = 0;
      lct = null;
    }

    /**
     * Skips variable length blocks up to and including next zero length block.
     */
    protected void skip() {
      do {
        readBlock();
      } while ((blockSize > 0) && !err());
    }
  }

}

サンプルプロジェクト

GifViewer.zip 直

apk

GifViewer.apk 直

デコメ絵文字

サンプルに含まれている画像はMovieクラスを使って表示しようとすると正常に表示されない画像です。

ちなみに自作です。

mspaintとバイナリエディタを使って描きました。

f:id:tomorrowkey:20100422195549g:image

f:id:tomorrowkey:20100422195550g:image

使ってください。