何度やっても同じ

ただの日記

ブロブストアに格納した画像データの扱い方

編集せずそのまま返したい場合

ふつうにBlobstoreからレスポンスに流し込めばおk。

BlobKey key = new BlobKey(asString("key"));
BlobstoreService blobstore = BlobstoreServiceFactory.getBlobstoreService();
blobstore.serve(key, response);

サムネイルを返したい場合

方法を3パターンに分けて紹介します。

ImagesService#getServingUrl をつかう

ImagesService#getServingUrlは、ブロブストアに格納した画像を取得する専用のURLを生成するメソッドです。つまり、毎回サムネイルが要求されるたびに実行するのではなく、画像がブロブストアにアップロードされたタイミングで一度だけURLの生成を行い、画像情報用のエンティティあたりに紐付けておくものです。

たとえばこんなモデルを作ってですね。。。

@Model(schemaVersion = 1)
public class Image implements Serializable {

    // keyプロパティやアクセサなどは省略

    private BlobKey blobKey;

    // ここにgetServingUrlで取得したURLを格納
    private String thumbnailUrl;
}

アップロード後の処理を行うコントローラでこんな感じでURLを生成しておきます。

// asBlobKeyの処理内容は察してね
BlobKey blobKey = asBlobKey("file");

String thumbnailUrl =
    ImagesServiceFactory.getImagesService().getServingUrl(
        ServingUrlOptions.Builder.withBlobKey(blobKey));

Image image = new Image();
image.setBlobKey(blobKey);
image.setThumbnailUrl(thumbnailUrl);
Datastore.put(image);

生成されるURLのメリットは高速であること、アプリケーションインスタンスが不要であることです。デメリットは、画像操作がリサイズとトリミングしかできないこと、publicなURLしか生成できないこと。とはいえサムネイル用途なら他の画像操作は通常不要ですし、URLは推測不可能な文字列なので、公開ページにリンクさえ張らなければ事実上のprivateなURLとしても扱えます。要するに、サムネイル用途ならgetServingUrlを使っておけば無難です。

getServingUrlにはオプションとして画像サイズやトリミングを行うかどうかの指定ができますが、これらの指定は一切しないことをオススメします。なぜかというと、サイズとトリミング指定は、基本となるURLにパラメータを付け加えることで実現できるからです。URLに固定でパラメータがついてると柔軟性がなくめんどくさいだけです。

URLにパラメータをつけるときのルールは以下の通り(Imagesサービスの概要ページからの抜粋)。URL末尾に =s32 みたいな文字列を付加するルールです。パラメータといってもクエリ文字列ではありません。

// Resize the image to 32 pixels (aspect-ratio preserved)
http://your_app_id.appspot.com/randomStringImageId=s32

// Crop the image to 32 pixels
http://your_app_id.appspot.com/randomStringImageId=s32-c

画像サイズの縦と横を別々に指定することはできません。縦横比率が維持されない使い方はできなくて(できる必要もないですが)、縦と横のうち長い方が指定サイズをオーバーしないように画像の縮小が行われます。

もうひとつささいなことですが個人的にはポイントだった特徴がありまして、getServingUrlのオプションで指定する画像サイズは、縮小されることがあっても拡大はされません。これはサムネイル生成の振る舞いとしては自然だと思うのですが、プログラムでブロブストアからImageオブジェクトを構築する手法(後述)ではこれを実現するのがなんだか面倒なのです。

画像が削除された場合などにURLを無効にしたい場合は、deleteServingUrlメソッドを使います。

ImagesServiceFactory.makeImageFromBlob をつかう

ブロブストアから画像を読み込んでImagesServiceで操作する手法です。以下のような感じ。binaryメソッドは勝手に作りました。処理内容は察してください。

Image image = ImagesServiceFactory.makeImageFromBlob(blobKey);
Transform resize = ImagesServiceFactory.makeResize(80, 80);
ImagesService service = ImagesServiceFactory.getImagesService();
image = service.applyTransform(resize, image, OutputEncoding.PNG);
byte[] data = image.getImageData();

return binary(data, "image/png");

この方法で困ってしまうのは、オリジナル画像が指定サイズよりも小さかった場合に、画像が拡大されてしまうことです。逆にそれを求めている場合や、その他(縮小とトリミング以外)の操作をしたい場合はgetServingUrlではなくこちらの方法を選択することになります。

ただ、また困ったことに、ブロブストアからmakeImageFromBlobで読み出したImageオブジェクトはいくつかのメソッドに対応しておらず、呼び出すとUnsupportedOperationExceptionを投げてきます。たとえば、上記の例で、画像の拡大はしたくないということで次のようなコードを書くと、Image#getWidthとImage#getHeightが使えないエラーが発生します。なんて理不尽なんでしょう。

Image image = ImagesServiceFactory.makeImageFromBlob(blobKey);
if (image.getHeight() > 80 || image.getWidth() > 80) {
    Transform resize = ImagesServiceFactory.makeResize(80, 80);
    // ...
}

これでは不便に感じることもありますよね。というわけで、最後のパターンです。

ブロブストアを自力で読み出してImageオブジェクトをつくる

サンプルソース書きます。単にブロブストアから読み出したデータでImageオブジェクトを作成しているだけなので、ふつうImagesServiceでできることは制限なく行えます。前記2つの方法でできないことをやりたい場合はこの方法を選択です。

BlobKey blobKey = new BlobKey(asString("id"));
BlobInfo info = new BlobInfoFactory().loadBlobInfo(blobKey);
int size = (int) info.getSize();
String contentType = info.getContentType();

BlobstoreService blobstore = BlobstoreServiceFactory.getBlobstoreService();
ByteArrayOutputStream out = new ByteArrayOutputStream();
int start = 0;

do {
    int end = Math.min(size, start + BlobstoreService.MAX_BLOB_FETCH_SIZE) - 1;
    byte[] buf = blobstore.fetchData(blobKey, start, end);
    out.write(buf);
    start = end + 1;
} while (start < size);

Image image = ImagesServiceFactory.makeImage(out.toByteArray());
if (image.getHeight() > 80 || image.getWidth() > 80) {
    Transform resize = ImagesServiceFactory.makeResize(80, 80);
    ImagesService service = ImagesServiceFactory.getImagesService();
    image = service.applyTransform(resize, image, OutputEncoding.PNG);
    contentType = "image/png";
}
byte[] data = image.getImageData();

return binary(data, contentType);