[HTML5] IndexedDBに画像ファイルを保存する - Dexie.js

IndexedDBは様々なデータ型に対応しておりblobも例外ではありません。つまり画像ファイルなどバイナリ形式の保存が可能というわけです。IndexedDBへ格納しておけば例えオフライン状態であっても好きなときに取り出して利用することができます。

単純にサーバ上にあるファイルをキャッシュしておくだけの用途であれば最近はWebWorkerCache APIを組み合わせた方が一般的ではありますが、これはデータに対してURLが存在することが前提です。例えばCanvasなどに描画した内容を保存しておきたいといった場合には今回の方法が重宝するかもしれません。

大まかな原理

IndexedDBを今回はDexie.jsを通して触ります。詳しい利用方法は過去の記事を参照してください。 blog.katsubemakito.net

IndexedDBへ保存する

適当な画像データを準備します。ここではFetch APIでサーバから取ってきていますが、Canvasの描画内容をblobで取得するにはtoBlob()メソッドを実行するだけです。その後、文字列や数値と同じ様にputすればIndexedDBへblobデータとして保存されます。簡単ですね! ※実際に利用するにはエラー処理を適宜挟んでくださいませ。

<script src="https://unpkg.com/dexie@latest/dist/dexie.min.js"></script>
<script>
window.onload = async ()=>{
  // 画像ファイルを取得し、blobへ変換
  const res  = await fetch("https://example.com/animal.png");
  const blob = await res.blob();

  // Canvasの描画内容をblobにする場合はtoBlob()メソッドを実行するだけ
  // const blob = document.querySelector("#canvas1").toBlob();

  // IndexedDBを準備
  const db = new Dexie("CacheDB");
  db.version(1)
    .stores({
      files: "name"
    });

  // IndexedDBへ保存
  db.files.put({name:"animal.jpg", data:blob});
}
</script>

ブラウザのデベロッパーツールでIndexedDBの状態を覗いてみると、次のように保存されているのが確認できました(以下はGoogleChrome)。

なお、ここでは画像ファイルで試していますが、音声ファイルなど他のファイル形式でも同じ要領で利用できます。

IndexedDBから取り出す

こちらもIndexedDBを開いたら、通常通りgetするだけ。その後取り出したデータをDOMを通じて流し込みます。ここでのポイントはURL.createObjectURL()を利用しているところです(後述します)。 ※実際に利用するにはエラー処理を適宜挟んでくださいませ。

<!-- このimgタグにIndexedDB内の画像をロードする -->
<img id="animal" style="display:none">

<script src="https://unpkg.com/dexie@latest/dist/dexie.min.js"></script>
<script>
window.onload = async ()=>{
  // IndexedDBを準備
  const db = new Dexie("CacheDB");
  db.version(1)
    .stores({
      files: "name"
    });

  // IndexedDBから取り出す
  const buff = await db.files.get("animal.jpg");

  // blobをObjectURLへ変換
  const objecturl = URL.createObjectURL(buff.data);

  // imgタグに差し込む
  const animal = document.querySelector("#animal");
  animal.setAttribute("src", objecturl);
  animal.style.display = "block";
}
</script>

ObjectURLとは?

URL.createObjectURL(data)を実行すると、メモリ上に引数として渡したデータを展開します。このメモリ上のデータにURLを通してアクセスすることを可能にする仕組みがObjectURLです。

URL.createObjectURL(data)の戻り値としてメモリにアクセスするためのURLが返されますが、通常は「blob:(オリジン)/(uuid)」といった書式になります。あとはこれを好きなところで表示するだけ。

小さいデータであればData URI schemeでも良いのですが、ファイルサイズが大きくなるほどエンコード/デコードなどのオーバーヘッドやサイズの肥大化がバカになりません。ObjectURLはデータがほぼそのままメモリ上に乗るためそういった心配がありません。

なお、ObjectURLで作成したデータはメモリ上に留まり続ける性質がありますので、使わなくなった場合にはURL.revokeObjectURL(objectURL)で開放する必要があります。

画像をIndexedDBへ保存する

サンプル

以下から実際に試していただけます。 miku3.net

  • ページの読み込みが完了したら次の処理を実行
    1. IndexedDBに画像データが存在していない場合はFetch APIでサーバから取得。
    2. IndexedDB内の画像データを表示

ソースコード

index.html

HTMLからメインの処理を担当するapp.js、IndexedDBへの保存や読み込みを担当するassetcache.jsの2つのJavaScriptが中心人物です。またassetcache.jsはDexie.jsに依存しているためCDNから読み込んでいます。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>File cache to IndexedDB + Dexie.js Sample1</title>
  <style>
    #disp{display: none;}
  </style>
</head>
<body>

<h1>画像をIndexedDBへキャッシュ</h1>

<!-- ローディング -->
<section id="loading">
  <img src="image/loading.svg" width="50"><br>
  ...画像を読込中
</section>

<!-- 画像を表示 -->
<section id="disp">
  <img id="chara">
</section>

<script src="https://unpkg.com/dexie@3/dist/dexie.min.js"></script>
<script src="js/assetcache.js"></script>
<script src="js/app.js"></script>
</body>
</html>

app.js

メインの処理を担当します。

/**
 * [イベント] ページの読み込みが完了
 */
window.onload = ()=>{
  loadImage("chara1", "image/chara.png");
}

/**
 * 画像をロードしてimgタグへセット
 *
 * @param {string} key IndexedDBへ保存する際の主キー
 * @param {string} url 画像のURL
 */
async function loadImage(key, url){
  const cache = new AssetCache();

  // IndexedDB内の存在チェック
  if( ! await cache.exists(key) ){
    await cache.setFetch(key, url);   // 存在しなければFetch APIで取得
  }

  // IndexedDB内のデータをObjectURLとして取得
  const data = await cache.getObjectURL(key);

  // ObjectURLをimgタグにセット
  document.querySelector("#chara").setAttribute("src", data);

  // 表示を切り替える
  document.querySelector("#loading").style.display = "none";
  document.querySelector("#disp").style.display = "block";
}

assetcache.js

IndexedDBとのやり取りを担当するAssetCacheクラスくんです。

/**
 * アセットをIndexedDBへキャッシュする
 *
 * @author M.Katsube
 * @version 1.0.0
 */
class AssetCache{
  /**
   * コンストラクタ
   *
   * @param {string} [db="AssetCache"]
   * @param {number} [version=1]
   */
  constructor(db="AssetCache", version=1){
    // データベースを開く
    this._db = new Dexie(db);

    // オブジェクトストアを開く
    this._db
          .version(version)
          .stores({
            files: "name"   // ファイル内容はインデックスしないこと
          });
  }

  /**
   * IndexedDBから指定された主キーの値を返却
   *
   * @param {any} key
   * @return {any} 存在しない場合はundefined
   */
  get(key){
    return( this._db.files.get(key) );
  }

  /**
   * IndexDBの指定主キーの値をObjectURLに変換して返却
   *
   * @param {any} key
   * @return {string|undefined}
   */
  getObjectURL(key){
    return(
      this
        .get(key)
        .then( (buff) => {
            if( buff !== undefined ){
              return( URL.createObjectURL(buff.data) );
            }
            return(undefined);
        })
    );
  }

  /**
   * 指定主キーが存在しているか確認
   *
   * @param {any} key
   * @returns {boolean}
   */
  exists(key){
    return( this
              .get(key)
              .then(value => value !== undefined) );
  }

  /**
   * 指定データをIndexedDBに保存する
   *
   * @param {any} key
   * @param {any} data
   * @returns {boolean}
   */
  set(key, data){
    return( this._db.files
              .put({name:key, data:data})
              .then(()=>{ return(true) }) );
  }

  /**
   * 指定URLのファイルをIndexDBに保存する
   *
   * @param {any} key
   * @param {string} url
   */
  setFetch(key, url){
    return( fetch(url)
              .then(res => res.blob())
              .then(blob => this.set(key, blob)) );
  }
}

参考ページ