Kugelblitz

いつ何時誰の挑戦でも受ける!

マルチページTIFFを分割する

JavaでマルチページTIFFを分割するコードを書いてみました。
書く前に軽くググッてみると、JAIを使う方法がヒットするんだけど、分割するだけならJAIはいらないんじゃないかな。あと、JAIの場合、分割先のファイルに、TIFFタグの情報とかコピーされないし。

TIFFファイルの仕様については、[CGファイル概説]がよくまとまっています。

また、テストのためのTIFFファイルは、以下からダウンロードしたものを使いました。

気楽な気持ちでコードを書き始めましたが、思ったより大変でした。StripOffsets、StripByteCountsの対応が特に大変です。Stripなんてしないで、まとめてイメージデータを格納してくれれば楽なんですが、Stripしないと、パフォーマンスに差がでるんだろうか?

というわけでコードは以下です。

package net.treewoods.sample_tiff;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;

public class TiffUtil {

    class IFD {
        int entryCount;
        List<IFD_Entry> entry = new ArrayList<>();
        IFD nextIFD;
    }

    class IFD_Entry {
        short tag;
        short dataType;
        int count;
        int data;

        @Override
        public String toString() {
            return "IFD_Entry{" + "tag=" + tag + ", dataType=" + dataType + ", count=" + count + ", data=" + data + '}';
        }
    }

    private static final int HEADER_SIZE = 8;
    private static final int IFD_ENTRY_SISE = 12;
    private static final byte[] DATA_SIZE = {0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8};
    private static final byte[] END = new byte[]{0x00, 0x00, 0x00, 0x00};

    /* データの型
     コード1…BYTE型(1バイト整数)
     コード2…ASCII型(1バイトのASCII文字)
     コード3…SHORT型(2バイト短整数)
     コード4…LONG型(4バイト長整数)
     コード5…RATIONAL型(8バイト分数、4バイトの分子とそれに続く4バイトの分母)
     コード6…SBYTE型(1バイト符号付き整数)
     コード7…UNDEFINED型(あらゆる1バイトデータ)
     コード8…SSHORT型(2バイト符号付き短整数)
     コード9…SLONG型(4バイト符号付き長整数)
     コード10…SRATIONAL型(8バイト符号付き分数、4バイトの分子とそれに続く4バイトの分母)
     コード11…FLOAT型(4バイト実数、IEEE浮動小数点形式)
     コード12…DOUBLE型(8バイト倍精度実数、IEEE倍精度浮動小数点形式)
     */

    /**
     * multi tiff を single tiff に分割する
     * tiffの仕様は以下を参照
     * http://www.snap-tck.com/room03/c02/cg/cg05_01.html
     * 
        // 1.ファイルヘッダ(末尾に最初のIFDへのポインタ)
        // 2.IFD
        // 3.画像関連データやカラーマップ(IFDエントリ内のデータポインタが指すデータ)
        // 4.イメージデータ
        // * ファイルヘッダ以外の順序は不定
        // ファイルヘッダを読み込み。保存しておく
        // IFDを読み込み。IFDのサイズ=2 + エントリカウント * 12 + 4
        // offset=0 バイトオーダー(2) "MM"(4D4DH) 正順 または"II"(4949H) 逆順
        // offset=2 version
        // offset=4 IFDポインタ(4)  > 直後なら0x00000008
        // IFD 
        // エントリカウント(2)
        // IFDエントリ0番(12)
        // ....
        // IFDポインタ(4) 次がなければ0
        // IFDエントリ
        // タグ(2)
        // データの型(2)
        // カウントフィールド(4)
        // データフィールドまたはデータポインタ(4)

        // データの型のサイズ * カウントフィールド が4byteを超えていたら、データポインタ
        // StripOffsets 固定値0111H(273) 各ストリップへのポインタ
        // StripByteCounts 固定値0117H(279) 各ストリップのサイズ long or short
         
     * @param input 分割対象ファイル名
     * @param outputDir 分割後イメージ出力ディレクトリ
     * @param outputFileName 分割後イメージファイル名。出力時には、ここで指定したファイル名にindex番号が付与される
     * @throws IOException 
     */
    public void splitMultiTiff(String input, String outputDir, String outputFileName) throws IOException {
        try (ImageInputStream is = ImageIO.createImageInputStream(new File(input))) {
            // header読み込み
            byte[] header = new byte[HEADER_SIZE];
            is.readFully(header);
            ByteOrder order;
            // バイトオーダーチェック
            if (header[0] == 0x49 && header[1] == 0x49) {
                order = ByteOrder.LITTLE_ENDIAN;
            } else {
                order = ByteOrder.BIG_ENDIAN;
            }

            // 先頭のIFDへのポインタ取得
            int idfOffset = binaryToInt(header, order, 4, 4);
            is.seek(idfOffset);

            IFD ifd = new IFD();

            // ヘッダのIFDポインタを書き換えておく
            // 分割後は、ヘッダの直後に置く
            byte[] p = intToBinary(HEADER_SIZE, order);
            header[4] = p[0];
            header[5] = p[1];
            header[6] = p[2];
            header[7] = p[3];

            int index = 0;
            while (true) {
                // TIFF内のイメージの数だけ繰り返し
                index++;
                try (FileOutputStream fos = new FileOutputStream(outputDir + outputFileName + index + ".tiff")) {
                    // ヘッダを書き込み
                    fos.write(header);

                    // IFDエントリカウント取得
                    byte[] buf = new byte[2];
                    is.readFully(buf);
                    fos.write(buf);
                    short entryCount = binaryToShort(buf, order);
                    System.out.println("INDEX:" + index + " ENTRY_COUNT:" + entryCount);
                    ifd.entryCount = entryCount;

                    // エントリカウントがわかると、データ格納開始位置が確定する
                    int dataOffset = HEADER_SIZE + 2 + IFD_ENTRY_SISE * entryCount + 4;

                    buf = new byte[IFD_ENTRY_SISE];
                    List<Integer> stripSize = new ArrayList<>();
                    List<Integer> stripOffset = new ArrayList<>();

                    for (int i = 0; i < entryCount; i++) {
                        is.readFully(buf);
                        IFD_Entry entry = new IFD_Entry();
                        entry.tag = binaryToShort(buf, order, 0, 2);
                        entry.dataType = binaryToShort(buf, order, 2, 2);
                        entry.count = binaryToInt(buf, order, 4, 4);
                        entry.data = binaryToInt(buf, order, 8, 4);
                        ifd.entry.add(entry);

                        System.out.println(entry.toString());

                        // 特別なタグへの対応
                        // StripOffsets 固定値0111H(273) 各ストリップへのポインタ
                        // StripByteCounts 固定値0117H(279) 各ストリップのサイズ long or short
                        if (entry.tag == 273) {
                            if (entry.count != 1) {
                                is.mark();
                                byte[] b = new byte[DATA_SIZE[entry.dataType]];
                                is.seek(entry.data);
                                for (int j = 0; j < entry.count; j++) {
                                    is.readFully(b);
                                    stripOffset.add(binaryToInt(b, order));
                                }
                                is.reset();
                            } else {
                                stripOffset.add(entry.data);
                            }
                        } else if (entry.tag == 279) {
                            if (entry.count != 1) {
                                is.mark();
                                byte[] b = new byte[DATA_SIZE[entry.dataType]];
                                is.seek(entry.data);
                                for (int j = 0; j < entry.count; j++) {
                                    is.readFully(b);
                                    stripSize.add(binaryToInt(b, order));
                                }
                                is.reset();
                            } else {
                                stripSize.add(entry.data);
                            }
                        }
                    }

                    // IFD書き込み
                    is.mark();
                    for (IFD_Entry e : ifd.entry) {
                        fos.write(shortToBinary(e.tag, order));
                        fos.write(shortToBinary(e.dataType, order));
                        fos.write(intToBinary(e.count, order));
                        if (e.tag != 273) {
                            if (DATA_SIZE[e.dataType] * e.count > 4) {
                                fos.write(intToBinary(dataOffset, order));
                                byte[] b = new byte[DATA_SIZE[e.dataType] * e.count];

                                is.seek(e.data);
                                is.readFully(b);
                                ByteBuffer allocate = ByteBuffer.wrap(b);

                                fos.getChannel().write(allocate, dataOffset);
                                dataOffset += DATA_SIZE[e.dataType] * e.count;
                            } else {
                                fos.write(intToBinary(e.data, order));
                            }
                        } else if (e.tag == 273) {
                            fos.write(intToBinary(dataOffset, order));
                            if (e.count > 1) {
                                //offset
                                int imgStart = dataOffset + e.count * DATA_SIZE[e.dataType];
                                for (int size : stripSize) {
                                    byte[] b;
                                    if (DATA_SIZE[e.dataType] == 2) {
                                        b = shortToBinary((short) imgStart, order);
                                    } else {
                                        b = intToBinary(imgStart, order);
                                    }

                                    ByteBuffer allocate = ByteBuffer.wrap(b);
                                    fos.getChannel().write(allocate, dataOffset);
                                    dataOffset += b.length;
                                    imgStart += size;
                                }
                            }

                            for (int j = 0; j < e.count; j++) {
                                byte[] b = new byte[stripSize.get(j)];
                                is.seek(stripOffset.get(j));
                                is.readFully(b);
                                ByteBuffer allocate = ByteBuffer.wrap(b);

                                fos.getChannel().write(allocate, dataOffset);
                                dataOffset += stripSize.get(j);
                            }
                        }
                    }
                    is.reset();

                    // 次のIFDポインタは0x00(シングルTIFFにするので)                    
                    fos.write(END);

                    // 次のIFD
                    buf = new byte[4];
                    is.readFully(buf);

                    // マルチTIFFの最後のイメージかチェック
                    int nextIDF = binaryToInt(buf, order);
                    if (nextIDF != 0) {
                        // 次のイメージあり
                        ifd.nextIFD = new IFD();
                        ifd = ifd.nextIFD;
                        is.seek(nextIDF);
                    } else {
                        // 最後のイメージ
                        break;
                    }
                }
            }
        }
    }

    protected static byte[] intToBinary(int src, ByteOrder order) {
        return ByteBuffer.allocate(4).order(order).putInt(src).array();
    }

    protected static byte[] shortToBinary(short src, ByteOrder order) {
        return ByteBuffer.allocate(2).order(order).putShort(src).array();
    }

    protected static int binaryToInt(byte[] src, ByteOrder order) {
        return ByteBuffer.wrap(src).order(order).getInt();
    }

    protected static int binaryToInt(byte[] src, ByteOrder order, int offset, int length) {
        return ByteBuffer.wrap(src, offset, length).order(order).getInt();
    }

    protected static short binaryToShort(byte[] src, ByteOrder order) {
        return ByteBuffer.wrap(src).order(order).getShort();
    }

    protected static short binaryToShort(byte[] src, ByteOrder order, int offset, int length) {
        return ByteBuffer.wrap(src, offset, length).order(order).getShort();
    }
}

たぶんどんなTIFFだって分割できるはず。

Pocket

他の記事