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;
  }
}