Exif情報について
デジカメやスマホで撮影した画像は通常JPEGというファイルフォーマットで保存されますが、これにはExif(Exchangeable image file format)という付属情報が付与されていて、そこに画像のサイズやら撮影日時やら撮影したカメラのメーカーやら、場合によっては位置情報なんかも保存されていたりします。SNSなどに画像を上げるときには注意してください。
で、たいていのデジカメでは、画像の方向(カメラを縦にして撮影した縦長の写真か、横にして撮影した横長の写真か)といった情報もExifに保持しています。つまり、カメラを縦にして縦長の写真を撮っても、画像データそのものは横長画像と同じように保持されていて、Exif情報により表示時にひっくりかえす、ということが期待されているわけです。時々、そのあたりの扱いがずれていて、画像がひっくり返って表示されることがあります。
Javaでは、標準のライブラリでは、Exif情報をもとに画像を正しい向きにする、という機能はないようなので、ファイルフォーマットの勉強も兼ねて、自力でExifフォーマットを読み込んでみました。
Exifフォーマットについて
Exifのフォーマットについては、主に以下のページを参照しました。
www2.airnet.ne.jp
beyondjapan.com
以下の仕様書の英語をちまちま拾い読みしていたら、
https://www.exif.org/Exif2-2.PDF
日本語版があるでやんの。もともと日本の企画ですからね……
http://www.cipa.jp/std/documents/j/DC-008-2012_J.pdf
ソースにコメントを過剰に入れておいたので、それを見ればフォーマットもわかるかと思います。
作ってみて
ソース
package com.kmaebashi.exifreadertest; import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.FileInputStream; import java.nio.ByteOrder; public class ExifReader { public static void main(String[] args) { String filePath = "JPEGファイルのパスをここに書いてください"; try (DataInputStream inStream = new DataInputStream( new BufferedInputStream( new FileInputStream(filePath)))) { // 冒頭12バイトの構成 // 先頭2バイト: 0xff 0xd8 JPEGファイルはこれで固定 // 次の2バイト: 0xff 0xe1 APP1(Application Marker Segment 1)のマーカー // 次の4バイト: APP1領域のサイズ(ビッグエンディアン) // 次の6バイト: Exifのマーカー('E', 'x', 'i', 'f', 00, 00) byte[] headersByte = new byte[12]; int[] headers; int app1Size; if (inStream.read(headersByte) != headersByte.length) { System.err.println("ファイルが小さすぎます。"); System.exit(1); } headers = unsignedByteArrayToIntArray(headersByte); if (headers[0] != 0xff || headers[1] != 0xd8) { System.err.println("JPEGファイルではありません。"); System.exit(1); } if (headers[2] != 0xff || headers[3] != 0xe1) { System.err.println("APP1データが含まれません。"); System.exit(1); } app1Size = read2Byte(headers, 4, ByteOrder.BIG_ENDIAN); if (headers[6] != (byte)'E' || headers[7] != (byte)'x' || headers[8] != (byte)'i' || headers[9] != (byte)'f' || headers[10] != 0x00 || headers[11] != 0x00) { System.err.println("Exifデータが含まれません。"); System.exit(1); } byte[] app1DataByte = new byte[app1Size]; if (inStream.read(app1DataByte) < app1Size) { System.err.println("ヘッダ情報に対し、ファイルサイズが小さすぎます。"); System.exit(1); } // Javaのbyteは符号付きでまともにバイトを扱えないので、intの配列に変換する。 int[] app1Data = unsignedByteArrayToIntArray(app1DataByte); // APP1の冒頭8バイトはTIFFヘッダ // 先頭2バイト: バイトオーダーを示す。 // 0x49, 0x49('I', 'I')...リトルエンディアン(Intelの略らしい) // 0x4d, 0x4d('M', 'M')...ビッグエンディアン(Motorolaの略らしい) // 次の2バイト: 0x00, 0x2a TIFF識別コード(固定) // 次の4バイト: ByteOrder byteOrder = ByteOrder.LITTLE_ENDIAN; // make compiler happy if (app1Data[0] == 0x49 && app1Data[1] == 0x49) { byteOrder = ByteOrder.LITTLE_ENDIAN; } else if (app1Data[0] == 0x4d && app1Data[1] == 0x4d) { byteOrder = ByteOrder.BIG_ENDIAN; } else { System.err.println("バイトオーダーが不正です。"); System.exit(1); } if (read2Byte(app1Data, 2, byteOrder) != 0x002a) { System.err.println("TIFF識別子が002aではありません。"); System.exit(1); } // 最初のIFD(Image File Directory)である0th IFDのオフセットを取得。 // これを含め、以後出てくるオフセットは、すべてAPP1の先頭を起点とする。 // ここまで、0th IFDのオフセットを含めて8バイト使っているので、 // その続きとなる0th IFDの先頭のオフセットはたいてい8。 int offsetOf0thIFD = read4Byte(app1Data, 4, byteOrder); System.out.println("0thIFDのオフセット(たいてい8)…" + offsetOf0thIFD); // 0th IFDのタグの数を取得(先頭2バイト) int numOf0thIFDTags = read2Byte(app1Data, offsetOf0thIFD, byteOrder); System.out.println("0thIFDのタグの数…" + numOf0thIFDTags); System.out.println("**** 0th IFD ****"); // 各タグについて内容出力。各タグは12バイトの固定長。 for (int i = 0; i < numOf0thIFDTags; i++) { // 8はTIFFヘッダ、2はタグの数の分 dumpIFDTag(app1Data, 8 + 2 + i * 12, byteOrder); } // Exif IFDがあれば、それも出力する。 if (exifOffset >= 0) { int numOfExifIFDTags = read2Byte(app1Data, exifOffset, byteOrder); System.out.println("**** EXIF IFD ****"); System.out.println("Exif IFDのタグの数…" + numOfExifIFDTags); for (int i = 0; i < numOfExifIFDTags; i++) { dumpIFDTag(app1Data, exifOffset + 2 + i * 12, byteOrder); } } } catch (Exception e) { e.printStackTrace(); System.exit(2); } } private static class TagType { public String name; public int size; public TagType(String name, int size) { this.name = name; this.size = size; } } // IDFのタグの型情報を保持する配列。 // Exifの仕様書(JEITA CP-3451) https://www.exif.org/Exif2-2.PDF を参照。 private static TagType[] tagTypeData = { null, new TagType("BYTE", 1), new TagType("ASCII", 1), // 末尾には'\0'が入る new TagType("SHORT", 2), new TagType("LONG", 4), new TagType("RATIONAL", 8), // 分数 null, new TagType("UNDEFINED", 1), null, new TagType("SLONG", 4), // 符号付 new TagType("SRATIONAL", 8), }; private static int exifOffset = -1; private static void dumpIFDTag(int[] array, int offset, ByteOrder byteOrder) { // ひとつのIFDタグ(固定長12バイト)の構成は以下の通り。 // 先頭2バイト: タグNo。定義はExifの仕様書(JEITA CP-3451)を参照。 // 次の2バイト: そのタグの型。 // 次の4バイト: そのタグに含まれる値の数。 // 次の4バイト: 値またはオフセット。示すべき値が4バイトに収まる場合はここに // 格納され、収まらない場合は、値の場所を示すオフセットが格納される。 int tagNo = read2Byte(array, offset, byteOrder); int tagType = read2Byte(array, offset + 2, byteOrder); int numOfValues = read4Byte(array, offset + 4, byteOrder); System.out.print(String.format("%04x", tagNo) + ":" + tagTypeData[tagType].name + ":" + numOfValues + ":"); if (tagTypeData[tagType].size * numOfValues <= 4) { printTagValue(array, offset + 8, tagType, numOfValues, byteOrder); System.out.println(""); } else { int valueOffset = read4Byte(array, offset + 8, byteOrder); printTagValue(array, valueOffset, tagType, numOfValues, byteOrder); System.out.println(""); } // 0th IFDの中にExif IFDのオフセット(タグNo.0x8769)があったら // static変数exitOffsetに退避する。 if (tagNo == 0x8769) { exifOffset = read4Byte(array, offset + 8, byteOrder); } } private static void printTagValue(int[] array, int offset, int tagType, int numOfValues, ByteOrder byteOrder) { for (int i = 0; i < numOfValues; i++) { if (i > 0) { System.out.print(", "); } if (tagTypeData[tagType].name == "ASCII") { if (array[offset + i] == 0) { System.out.print("'\\0'"); } else { System.out.print("" + (char)array[offset + i]); } } else if (tagTypeData[tagType].size == 1) { int value = array[offset + i]; System.out.print(String.format("%02x", value)); } else if (tagTypeData[tagType].size == 2) { int value = read2Byte(array, offset + (i * 2), byteOrder); System.out.print(String.format("%04x", value)); } else if (tagTypeData[tagType].size == 4) { int value = read4Byte(array, offset + (i * 4), byteOrder); System.out.print(String.format("%08x", value)); } else if (tagTypeData[tagType].size == 8) { int numerator = read4Byte(array, offset + (i * 8), byteOrder); int denominator = read4Byte(array, offset + (i * 8) + 4, byteOrder); System.out.print("" + numerator + "/" + denominator); } } } private static int[] unsignedByteArrayToIntArray(byte[] src) { int[] dest = new int[src.length]; for (int i = 0; i < src.length; i++) { dest[i] = Byte.toUnsignedInt(src[i]); } return dest; } public static int read2Byte(int[] array, int offset, ByteOrder byteOrder) { int ret; if (byteOrder == ByteOrder.BIG_ENDIAN) { ret = array[offset] * 256 + array[offset + 1]; } else { ret = array[offset + 1] * 256 + array[offset]; } return ret; } public static int read4Byte(int[] array, int offset, ByteOrder byteOrder) { int ret; if (byteOrder == ByteOrder.BIG_ENDIAN) { ret = array[offset] * (256 * 256 * 256) + array[offset + 1] * 65536 + array[offset + 2] * 256 + array[offset + 3]; } else { ret = array[offset + 3] * (256 * 256 * 256) + array[offset + 2] * 65536 + array[offset + 1] * 256 + array[offset]; } return ret; } }
出力例
Galaxy Note 8で撮った縦長写真です。先日しまなみ海道に行った時のやつ。
0thIFDのオフセット(たいてい8)…8 0thIFDのタグの数…13 **** 0th IFD **** 0100:LONG:1:00000fc0 0101:LONG:1:00000bd0 010f:ASCII:8:s, a, m, s, u, n, g, '\0' 0110:ASCII:7:S, C, -, 0, 1, K, '\0' 0112:SHORT:1:0006 ←これが画像の向きを示す。6は、時計回りに90°回せば元に戻る向き。 011a:RATIONAL:1:72/1 011b:RATIONAL:1:72/1 0128:SHORT:1:0002 0131:ASCII:14:S, C, 0, 1, K, O, M, U, 1, C, S, G, 3, '\0' 0132:ASCII:20:2, 0, 1, 9, :, 0, 9, :, 1, 4, , 1, 8, :, 0, 0, :, 5, 4, '\0' 0213:SHORT:1:0001 8769:LONG:1:000000ec 8825:LONG:1:0000175a **** EXIF IFD **** Exif IFDのタグの数…31 829a:RATIONAL:1:1/40 829d:RATIONAL:1:170/100 8822:SHORT:1:0002 8827:SHORT:1:00c8 9000:UNDEFINED:4:30, 32, 32, 30 9003:ASCII:20:2, 0, 1, 9, :, 0, 9, :, 1, 4, , 1, 8, :, 0, 0, :, 5, 4, '\0' 9004:ASCII:20:2, 0, 1, 9, :, 0, 9, :, 1, 4, , 1, 8, :, 0, 0, :, 5, 4, '\0' 9101:UNDEFINED:4:01, 02, 03, 00 9201:SRATIONAL:1:5321/1000 9202:RATIONAL:1:153/100 9203:SRATIONAL:1:93/100 9204:SRATIONAL:1:0/10 9205:RATIONAL:1:153/100 9207:SHORT:1:0002 9208:SHORT:1:0000 9209:SHORT:1:0000 920a:RATIONAL:1:430/100 927c:UNDEFINED:98:07, 00, 01, 00, 07, 00, 04, 00, 00, 00, 30, 31, 30, 30, 02, 00, 04, 00, 01, 00, 00, 00, 00, 20, 01, 00, 0c, 00, 04, 00, 01, 00, 00, 00, 00, 00, 00, 00, 10, 00, 05, 00, 01, 00, 00, 00, 5a, 00, 00, 00, 40, 00, 04, 00, 01, 00, 00, 00, 00, 00, 00, 00, 50, 00, 04, 00, 01, 00, 00, 00, 01, 00, 00, 00, 00, 01, 03, 00, 01, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 9286:UNDEFINED:5107:41, 53, 43, 49, 49, 00, 00, 00, 0a, 00, 00, 00, <なんかいっぱい出てるので中略> 30, 20, 00 a000:UNDEFINED:4:30, 31, 30, 30 a001:SHORT:1:0001 a002:LONG:1:00000fc0 a003:LONG:1:00000bd0 a005:LONG:1:0000173c a217:SHORT:1:0002 a301:UNDEFINED:1:01 a402:SHORT:1:0000 a403:SHORT:1:0000 a405:SHORT:1:001a a406:SHORT:1:0000 a420:ASCII:24:G, 1, 2, Q, S, K, A, 0, 2, S, M, , G, 1, 2, Q, S, K, D, 0, 1, S, A, '\0'