🍃このブログは移転しました。
3秒後、自動的に移動します・・・。

OpenSeadragonで、非同期にgetTileUrl()したい

高機能な画像ビューワーであるOpenSeadragon(以下、OSD)を使った小ネタ。

GitHub - openseadragon/openseadragon: An open-source, web-based viewer for zoomable images, implemented in pure JavaScript.

カスタムタイルソース

OSDでは、IIIFやDZIのように決められたフォーマット以外にも、生のタイル画像をそのまま扱えるパターンもある。

で、それをやるにはこんなコードを書く。

new OpenSeadragon.Viewer({
  // ...

  tileSources: {
    height: 512 * 256,
    width: 512 * 256,
    tileSize: 256,
    getTileUrl: (level, x, y) =>
      "http://s3.amazonaws.com/com.modestmaps.bluemarble/"
        + `${level - 8}-r${y}-c${x}.jpg`,
  },
});

ここまでは、ドキュメントにも書いてある通り。

Custom Tile Source | OpenSeadragon

この場合の実装的な挙動としては、内部的に`Image`オブジェクトを生成して、その`src`にこの`getTileUrl()`で得られるパスがそのまま使われるようになってる。

で、これの問題は、タイル画像に対して、静的なパス + 単なるGETでアクセスできない場合。

  • 特別なリクエストヘッダーが必要
  • POSTじゃないとダメ
  • SDKを通して非同期にバイナリを渡す
  • そもそも画像は別のパイプラインで生成する
  • etc...

などなどの場合に、どうするか。

1. オプションで解決する

実は`Viewer`や`TileSources`を指定するときに、いくつか渡せるオプションがあって、それで挙動を変えて解決できるものもある。

  • `crossOriginPolicy`: `Image`で読み込む場合の`crossorigin`属性
  • `loadTilesWithAjax`: `true`なら、`XMLHttpRequest`のラッパーである`$.makeAjaxRequest()`でGETするようになる
    • `ajaxHeaders`: その際のカスタムヘッダー
    • `ajaxWithCredentials`: その際の`withCredentials`

単にヘッダがついてればいい場合は、こういうのでいい。

ほかにも、`tileSources`を文字列URLで指定した場合に、そのURLの`#`以降をPOSTで送信できる?風の、`splitHashDataForPost`なるオプションもあるらしい。
(というか、高機能すぎて何ができるのか未だに把握できないし、この場合はこれが使えないとかそういうのもわかってない。)

2. コードを上書きして解決する

オプションだけでは解決できない場合の奥の手。
たとえばS3とかにタイル画像を置いてて、SDKでしかそれを取得できない場合などに。

さっきから見ての通り、`getTileUrl()`のシグネチャは`() => string`ではあるが、`OpenSeadragon.TileSource`クラスの一部を差し替えることで、非同期にできる。

これを事前に実行しておけば、`getTileUrl()`のシグネチャを`() => Promise`にできるってわけ。
`() => string`以外を返すようにした場合は、`hasTransparency(): boolean`がエラーにならないようにケアする必要があるらしい。

もしくは、`getTileUrl()`はそのまま同期で文字列を返しつつ、ロジック側でよしなに非同期することもできる。

const url = context.src;

const ac = new AbortController();
mySdk.getObject(mySdk.toParams(url), ac.signal)
  .then((blob) => {
    image.src = URL.createObjectURL(blob);
  })
  .catch((err) => finish(err.message));

// XXX: Shoud be `XMLHttpRequest` but `abort()` is only used
dataStore.request = ac;

なんかのタイミングで`revokeObjectURL()`したい気持ちもあるけど、もともとのロジックでもやってなかったので仕方ない。

というわけで、ココのあたりをいじれば、だいたいのことは実現できそうである。ただし、裏技なので本体アップデートによりいつ動かなくなってもおかしくない。

OSDはすごい

高機能ではあるが、この時代にESMでもなく3万行の巨大な1ファイルしか提供されてないし、コードも歴史ある感じでメンテしやすいとも言えず、かといって代わりになるライブラリもない・・って感じ。

リポジトリのIssueを見てた限り、この非同期の取得はそれなりに需要あることらしかった。なので、今回の対応もできれば本家コードに馴染むように、できればPRも・・・とやってたけど、そう簡単でもないな〜って感じたのでやめた。

それでもばっちり動いてるからすごい。