Javaで画像のExifを読み込む - metadata-extractor


# 概要
JavaでExifを読み込むmetadata-extractorというライブラリがあります。
幅広いフォーマットを扱えて、動画やメーカーノートにも対応しているようです。
今回はこのライブラリでどのようにExifを扱えるのかを試してみます。
Javaの他にC#にも対応しています。

# 環境
Windows 10 Pro 64bit
Java SE 8 Update 102 64bit
metadata-extractor 2.9.1

# 準備
## Maven
Maven Centralに登録されているので、下記の依存情報を追加します。

```xml:pom.xml
<dependency>
  <groupId>com.drewnoakes</groupId>
  <artifactId>metadata-extractor</artifactId>
  <version>2.9.1</version>
</dependency>
```

## 手動でjarを追加する場合
プロジェクトに手動でjarを追加する場合、下記のjarが必要です。

* metadata-extractor-2.9.1.jar
* xmpcore-5.1.2.jar

XMP Library For Javaというライブラリに依存しています。
現時点でxmpcoreの最新は6.1.10でしたが、metadata-extractorが依存しているのはちょっと古いバージョンのようです。

# 実装
## 全体的な流れ
metadata-extractorでは、下記の手順でExifを取得します。

1. 画像ファイルからMetadataを取得
2. MetadataからDirectoryを取得
3. DirectoryからTagを取得、参照

MetadataはExif情報を1つに集めたもの。画像1ファイルにつき、1つのMetadataを取得できます。
DirectoryはExifのIFD0やSubIFDなど、カテゴリのようなものを指します。
TagがImageWidthなどの項目に該当します。

## 一番簡単なサンプル
下記コードで画像ファイルのすべてのExif情報を表示できます。

```java
Metadata metadata = ImageMetadataReader.readMetadata(new File("xxxx.jpg"));
for (Directory directory : metadata.getDirectories()) {
    for (Tag tag : directory.getTags()) {
        System.out.println(tag);
    }
}
```

```text:実行結果
[JPEG] Compression Type - Baseline
[JPEG] Data Precision - 8 bits
[JPEG] Image Height - 2448 pixels
[JPEG] Image Width - 3264 pixels
...
```

## Metadata
### 基本

ImageMetadataReader#readMetadata()にInputStreamを渡すことでMetadataを取得します。
画像の種類は自動で判別してくれます。
下記コードではクラスパスからの相対パスで画像ファイルを特定している為、野暮ったくなっていますが、
画像ファイルのInputStreamが取得できれば別の方法(FileInputStreamなど)でも構いません。

```java:InputStreamから取得
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try (InputStream input = classLoader.getResourceAsStream("images/black_iphone6.jpg")) {
    Metadata metadata = ImageMetadataReader.readMetadata(input);
}
```

Fileクラスを渡してもいいです。
NIO.2には対応しておらず、Pathは渡せません。

```java:Fileから取得
File file = new File("src/main/resources/images/black_iphone6.jpg");
Metadata metadata = ImageMetadataReader.readMetadata(file);
```

### 画像の種類が判明している場合
画像の種類に応じたMetadataReaderが提供されているので、そちらを使用します。
判別処理を行わない分、ごくわずかに性能向上が見込めます。

```java
Metadata metadata = JpegMetadataReader.readMetadata(input);
```

### 読み込むDirectoryを指定
必要なDirectoryのみ読み込むように指定することが可能です。
大量の画像を扱うなど、性能が求められる場合に使うといった感じでしょうか。

readMetadataの引数に、JpegSegmentMetadataReaderの実装クラスのインスタンスを渡します。
この方法は現状ではJpegのみ対応しているようです。

```java
Iterable<JpegSegmentMetadataReader> readers = Arrays.asList(new JpegReader(), new IccReader());
Metadata metadata = JpegMetadataReader.readMetadata(input, readers);
```

## Directory
### Metadataが保持するすべてのDirectoryを取得
全てのDirectoryを取得するには、getDirectories()を使用します。

```java
Iterable<Directory> directories = metadata.getDirectories();
```

### 指定したDirectoryのみ取得
指定した型のDirectoryのみ取得する場合は、getDirectoriesOfType()を使用します。
引数にはDirectoryクラスを継承するクラスを指定します。
下記コードの場合、ExifDirectoryBaseを継承する5種類のDirectoryを取得できます。

```java
Collection<ExifDirectoryBase> exifDirectories = metadata.getDirectoriesOfType(ExifDirectoryBase.class);
```

なお、代入している変数の型は、メソッドのシグネチャに合わせています。
Iterable使ったり、Collection使ったり、統一感ないという印象があります。

### 指定したDirectoryで最初の1件のみ取得
引数で渡すDirectoryクラスによっては、複数のDirectoryが合致する場合(ExifDirectoryBaseなど)があります。
getFirstDirectoryOfType()を使うと、最初にヒットした1件のみを取得できます。

```java
JpegDirectory jpegDirectory = metadata.getFirstDirectoryOfType(JpegDirectory.class);
```

## Tag
Exifの各項目に相当するTagの扱いです。
Tagクラスのオブジェクトを取得して参照する方法と、Directoryオブジェクトから参照する方法があります。

### Tagクラス
Directory#getTags()でそのDirectoryが保持する全てのTagを取得できます。

```java
for (Tag tag : directory.getTags()) {
}
```

Tagクラスからは以下のような情報を取得できます。

```java
String directoryName = tag.getDirectoryName(); // JPEG
String tagName = tag.getTagName(); // Image Height
int tagType = tag.getTagType(); // 1
String tagTypeHex = tag.getTagTypeHex(); // 0x0001
String description = tag.getDescription(); // 2448 pixels
```

項目の値はgetDescription()で取得できますが、上記の通り"pixels"が付与された文字列になります。
逆に2448という値そのものを取得するには、後述するDirectoryから取得する方法を使う必要があります。

### DirectoryからTagの値を取得する
下記のgetInt()のように、型ごとに用意されたgetterを使って取得します。
取得したい値の型に合わせて使い分ける必要があります。

```java
if (directory.hasTagName(JpegDirectory.TAG_IMAGE_HEIGHT)) {
    int imageHeight = directory.getInt(JpegDirectory.TAG_IMAGE_HEIGHT); // 2448
}
```

抽象クラスのDirectoryではなく、継承する実装クラスを取得すれば、固有のgetterが提供されています。

```java
JpegDirectory jpegDirectory = metadata.getFirstDirectoryOfType(JpegDirectory.class);
int imageHeight = jpegDirectory.getImageHeight(); // 3264
int imageWidth = jpegDirectory.getImageWidth(); // 2448
```

### TagDescriptor
前述の通り、getDescription()を使うと、装飾された値が取得できます。
この装飾はTagDescriptorにより実現しており、独自に実装することが可能です。

この値の装飾はTagDescriptorという仕組みで実現しています。
自前のTagDescriptorを渡すことで、独自に値の装飾を施すことが可能です。

```java
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try (InputStream input = classLoader.getResourceAsStream("images/black_iphone6.jpg")) {
    Metadata metadata = ImageMetadataReader.readMetadata(input);
    JpegDirectory jpegDirectory = metadata.getFirstDirectoryOfType(JpegDirectory.class);
    jpegDirectory.setDescriptor(new JpegDescriptor(jpegDirectory) {
        @Override
        public String getImageHeightDescription() {
            return String.format("%spx", _directory.getString(TAG_IMAGE_HEIGHT));
        }
    });
    System.out.println(jpegDirectory.getDescription(TAG_IMAGE_HEIGHT));
}
```

TagDescriptorとDirectoryが相互参照していたり、メソッドの構成など、ちょっと分かりにくい印象です。

# まとめ
できることはExifの読み込みのみです。
書き換えもできると、用途の幅が広がりそうなのですが。
とはいえ、幅広いフォーマットに対応しており、Exifを参照するアプリケーションを作れるのはありがたい存在です。

# 参考
[metadata-extractor — jpeg exif / iptc metadata extraction for java and c# ](https://drewnoakes.com/code/exif/)
[GitHub - drewnoakes/metadata-extractor: Extracts Exif, IPTC, XMP ... ](https://github.com/drewnoakes/metadata-extractor)