TIFFファイルの仕様については、以下がまとまっていると思います。
また、テストのための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の仕様は以下を参照
* CGファイル概説 第5章 第1節
*
// 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だって分割できるはず。