JavaFX - Image をバックグラウンドでロードする

# デフォルトは同期的にロードされる
`Image` のインスタンスは、普通に生成すると画像のロードが同期的に行われる（ロードが終了するまで処理は返ってこない）。

画像が小さい場合は特に問題はないが、大きいサイズの画像を表示する場合は処理がしばらく停止してしまうので、操作感が損なわれる恐れがある。

# バックグラウンドでロードする
`Image` インスタンスでの画像のロードをバックグラウンドで行うには、コンストラクタ引数の `backgroundLoading` に `true` を指定する。
`backgroundLoading` を指定できるコンストラクタは、次の２つがある。

- [Image(String, boolean)](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/image/Image.html#Image-java.lang.String-boolean-)
- [Image(String, double, double, boolean, boolean, boolean)](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/image/Image.html#Image-java.lang.String-double-double-boolean-boolean-boolean-)

`backgroundLoading` に `true` を指定してインスタンスを生成すると、画像は非同期でロードされる。
これにより、処理は止まることなく次に進むことができるようになる。

# ロードの進捗を表示する
ただ単純にロードをバックグラウンドにしただけだと、読み込みが完了するまで画像が表示されない状態が続く。

サイズが大きくロードに時間がかかる場合、画像が何も表示されないというのはユーザを不安にさせてしまうかもしれない。
そこで、ロードがどれくらい進んでいるか、進捗をプログレスバーなどで表示すると良いかもしれない。

`Image` クラスは、ロードの進捗を知ることができる [progress](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/image/Image.html#progressProperty) プロパティを提供している。
これを利用すれば、わりと簡単にロードの進捗を表示できる。

## 実装
![WS000000.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/fa2de237-d332-8581-c86d-1a266651470b.jpeg)

```xml:main.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>

<BorderPane style="-fx-padding: 10px;" xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.javafx.MainController">
   <center>
      <VBox alignment="CENTER" minHeight="0.0" minWidth="0.0" BorderPane.alignment="CENTER">
         <children>
            <ProgressBar fx:id="progressBar" maxWidth="1.7976931348623157E308" progress="0.0" />
            <ImageView fx:id="imageView" fitHeight="200.0" fitWidth="300.0" pickOnBounds="true" preserveRatio="true" />
         </children>
      </VBox>
   </center>
</BorderPane>
```

```java:MainController.java
package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ProgressBar;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.net.URL;
import java.nio.file.Paths;
import java.util.ResourceBundle;

public class MainController implements Initializable {
    @FXML
    private ProgressBar progressBar;
    @FXML
    private ImageView imageView;

    public void initStage(Stage stage) {
        stage.setWidth(500);
        stage.setHeight(400);
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        progressBar.managedProperty().bind(progressBar.visibleProperty());
        progressBar.visibleProperty().bind(progressBar.progressProperty().lessThan(1));
        
        imageView.managedProperty().bind(imageView.visibleProperty());
        imageView.visibleProperty().bind(progressBar.progressProperty().isEqualTo(1));
        
        Image image = new Image(Paths.get("./image/shirakawago.jpg").toUri().toString(), true);
        progressBar.progressProperty().bind(image.progressProperty());
        imageView.setImage(image);
    }
}
```

## 実行結果
![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/3dd003f5-5d7b-1d86-3dbe-3c221ef75b8e.gif)

ちょっと早くてわかりづらいが、画像をロードしている間はプログレスバーが表示され、ロードが完了すると画像が表示されている。

## 説明
![WS000000.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/fa2de237-d332-8581-c86d-1a266651470b.jpeg)

- 画像表示のための `ImageView` のとなりに `ProgressBar` を追加している

```java:MainController.java
        progressBar.managedProperty().bind(progressBar.visibleProperty());
        progressBar.visibleProperty().bind(progressBar.progressProperty().lessThan(1));
        
        imageView.managedProperty().bind(imageView.visibleProperty());
        imageView.visibleProperty().bind(progressBar.progressProperty().isEqualTo(1));
```

- ロード中はプログレスバーのみを表示しロードが完了したら画像だけを表示するため、それぞれの `visible` プロパティを制御している
- `visible` プロパティの制御は、プログレスバーの `progress` プロパティの値にバインドすることで実現している
    - プログレスバーは、 `progress` が `1` より小さいときだけ表示
    - 画像は、 `progress` が `1` のときだけ表示、としている

```java:MainController.java
        Image image = new Image(Paths.get("./image/shirakawago.jpg").toUri().toString(), true);
        progressBar.progressProperty().bind(image.progressProperty());
```

- `Image` を生成するときのコンストラクタ引数で、 `backgroundLoading` に `true` を指定
- `Image` の `progress` プロパティをプログレスバーの `progress` プロパティにバインドしている

# あらかじめロードしておく
## メリット
バックグラウンドでのロードは、現在表示しようとしている画像だけでなく、次に表示する予定の画像をあらかじめロードしておくといった手段でも利用できる。

この場合、 `Image` インスタンスだけを裏で生成しておき、画像を切り替えるときに `ImageView` の `setImage()` で `Image` インスタンスを差し替えるように実装する。

```java:イメージ
@FXML
private ImageView imageView;
private Image nextImage;

public void initialize(URL location, ResourceBundle resources) {
    // バックグラウンドで次の画像をロードしておく
    nextImage = new Image("next-image.jpg", true);

    Image initialImage = new Image("initial-image.jpg", true);
    imageView.setImage(initialImage);
}

@FXML
public void nextPage() {
    // ロードしておいた次の画像をセットする
    imageView.setImage(nextImage);
}
```

重い画像でも、あらかじめロードしておくことで素早く表示が実現できるようになり、操作感の向上につながるかもしれない。

## メモリ使用量
事前にロードをしておくことで表示速度は向上するかもしれないが、その代わりメモリの消費は増えることに注意しなければならない。

特に、画像はディスク上でのサイズとメモリ上にロードしたときのサイズには大きな差があることに気を付ける必要がある。
ディスク上では、例えば JPEG 画像の場合は大幅に圧縮されたサイズになっているのに対して、 `Image` でメモリ上にロードした場合は非圧縮状態になっている。

実際にどれくらい違うが調べてみる。

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/cee882d2-536c-a659-9fe2-54912d1cba64.jpeg)

こちらがディスク上でのサイズで、およそ 6.5 MB ある。

この画像を `Image` でロードしてから、ヒープダンプを取って `Image` がロードした画像がどれくらいメモリを使用しているか調べてみる。
ヒープダンプの調査には Eclipse Memory Analyzer を使用した。

---
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/ccf8e234-91de-52d9-1389-4c6348642123.jpeg)

`Image` の中の `platformImage` がそれっぽいので、その中を見に行く。

---
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/fe4e9e02-688e-01e9-b111-1ab589297719.jpeg)

`com.sum.prism.Image` がそれっぽいので、その中を見に行く。

---
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/26eddd3a-b6e4-a5fb-1af9-e0b3b3317049.jpeg)

`width`, `height` に画像のピクセルサイズが格納されている。
さらに、 `pixelBuffer` が画像データをバイト配列で格納しているっぽいので、その中を見に行く。

---
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/a58fc13e-cac4-2cdd-9923-330221572512.jpeg)

ビンゴっぽい。
`capacity` が `36636672` となっている（約 35 MB）。
調べてみると `hb` というのが `byte` の配列になっていて、画像データが全て格納されているっぽい。

この `36,636,672` というサイズは、ちょうど画像の「縦×横×３」のサイズに一致している。
（`4,288 * 2,848 = 12,212,224`, `12,212,224 * 3 = 36,636,672`）

つまり、全ピクセルの RGB (3byte) 分のデータが全てロードされているということなのだろう。

ディスク上でのサイズが約 6.5 MB なのに対して、メモリ上にロードしたときは 35 MB とかなり大きくなっている。

元画像の圧縮形式や、アルファチャンネル（透明度）の有無によってはディスク上とメモリ上でのサイズの差は色々変わると思う。

## サイズを小さくしてロードする
`Image` は画像のサイズを指定してロードができるようになっている。

- [Image(String, double, double, boolean, boolean, boolean)](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/image/Image.html#Image-java.lang.String-double-double-boolean-boolean-boolean-)

これを利用すれば、オリジナルよりも小さいサイズで画像をロードできるので、メモリ消費量を抑えることができる。
ただし、小さいサイズでロードすることになるので、元画像と同じ品質では表示できないことに注意。

## まとめ
バックグラウンドでのロードは表示速度の向上につながるかもしれないが、事前ロードによるキャッシュ利用をする場合はメモリの消費量にも注意したほうが良さげ。