🌟

Mirador 4で任意の領域をハイライト表示する方法

に公開

はじめに

IIIFビューアのMiradorには検索機能があり、IIIF Search APIに対応したマニフェストでは検索結果をハイライト表示できます。しかし、Search APIに非対応のマニフェストでも、任意の領域をハイライト表示したいケースがあります。

本記事では、Miradorの内部APIを利用して、外部データソースからのアノテーション情報を基にハイライト表示を実現する方法を紹介します。

デモ

ユースケース

  • 独自のOCRシステムで抽出したテキスト領域のハイライト
  • 機械学習で検出したオブジェクトの領域表示
  • 外部データベースに保存されたアノテーションの可視化
  • Search API非対応のIIIFサーバーでの検索結果表示

実装方法

基本的な仕組み

Miradorは内部でReduxを使用しており、receiveSearchアクションを通じて検索結果を登録できます。このアクションにIIIF Search API形式のJSONを渡すことで、任意のデータソースからのハイライトを表示できます。

必要な情報

ハイライトを表示するために必要な情報は以下の3つです:

  1. キャンバスURI - ハイライトを表示するページのURI
  2. 座標(xywh) - ハイライト領域の位置とサイズ(x, y, width, height)
  3. テキスト - ハイライトに関連付けるテキスト(検索パネルに表示される)

サンプルコード

以下は、国立国会図書館デジタルコレクションの源氏物語で「いつれの御時にか...」の冒頭部分をハイライト表示するサンプルです。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Mirador Custom Highlight Sample</title>
  <style>
    body { margin: 0; padding: 0; }
    #mirador-viewer { width: 100%; height: 100vh; }
  </style>
</head>
<body>
  <div id="mirador-viewer"></div>

  <script src="https://unpkg.com/mirador@4.0.0-alpha.15/dist/mirador.min.js"></script>
  <script>
    // 設定パラメータ
    const config = {
      manifestUrl: 'https://dl.ndl.go.jp/api/iiif/3437686/manifest.json',
      canvasId: 'https://dl.ndl.go.jp/api/iiif/3437686/canvas/22',
      highlights: [
        {
          xywh: '3095,694,97,2051',
          text: 'いつれの御時にか女御更衣あまたさふらひ給けるなかにいとやむことなきゝは',
        },
      ],
    };

    // Miradorを初期化
    const miradorViewer = Mirador.viewer({
      id: 'mirador-viewer',
      selectedTheme: 'light',
      language: 'ja',
      windows: [{
        id: 'window-1',
        manifestId: config.manifestUrl,
        canvasId: config.canvasId,
        thumbnailNavigationPosition: 'far-right',
      }],
      window: {
        allowFullscreen: true,
        allowClose: false,
        allowMaximize: false,
        sideBarOpen: true,
      },
      workspaceControlPanel: {
        enabled: false,
      },
    });

    // ハイライトを追加する関数
    function addHighlights(viewer, canvasId, highlights) {
      // IIIF Search API形式のレスポンスを構築
      const searchResponse = {
        '@context': 'http://iiif.io/api/search/1/context.json',
        '@id': canvasId + '/search',
        '@type': 'sc:AnnotationList',
        within: {
          '@type': 'sc:Layer',
          total: highlights.length,
        },
        resources: highlights.map((highlight, index) => ({
          '@id': canvasId + '/highlight-' + index,
          '@type': 'oa:Annotation',
          motivation: 'sc:painting',
          resource: {
            '@type': 'cnt:ContentAsText',
            chars: highlight.text,
          },
          on: canvasId + '#xywh=' + highlight.xywh,
        })),
      };

      // 検索パネルを右側に追加
      const addAction = Mirador.addCompanionWindow('window-1', {
        content: 'search',
        position: 'right',
      });
      viewer.store.dispatch(addAction);

      // companionWindowIdを取得
      const state = viewer.store.getState();
      const searchCompanionWindowId = Object.keys(state.companionWindows).find(
        id => state.companionWindows[id].content === 'search'
      );

      if (searchCompanionWindowId) {
        // 検索結果を登録
        const searchAction = Mirador.receiveSearch(
          'window-1',
          searchCompanionWindowId,
          canvasId + '/search',
          searchResponse
        );
        viewer.store.dispatch(searchAction);
      }
    }

    // マニフェストの読み込み完了を監視してハイライトを追加
    let highlightAdded = false;
    const unsubscribe = miradorViewer.store.subscribe(() => {
      if (highlightAdded) return;

      const state = miradorViewer.store.getState();
      const manifests = state.manifests || {};
      const manifest = manifests[config.manifestUrl];

      // マニフェストが読み込み完了したらハイライトを追加
      if (manifest && !manifest.isFetching && manifest.json) {
        highlightAdded = true;
        unsubscribe();
        addHighlights(miradorViewer, config.canvasId, config.highlights);
      }
    });
  </script>
</body>
</html>

コードの解説

1. 設定パラメータ

const config = {
  manifestUrl: 'https://dl.ndl.go.jp/api/iiif/3437686/manifest.json',
  canvasId: 'https://dl.ndl.go.jp/api/iiif/3437686/canvas/22',
  highlights: [
    {
      xywh: '3095,694,97,2051',
      text: 'いつれの御時にか...',
    },
  ],
};
  • manifestUrl: IIIFマニフェストのURL
  • canvasId: ハイライトを表示するキャンバスのURI
  • highlights: ハイライト情報の配列。複数のハイライトを追加可能

2. IIIF Search API形式のレスポンス構築

const searchResponse = {
  '@context': 'http://iiif.io/api/search/1/context.json',
  '@type': 'sc:AnnotationList',
  resources: highlights.map((highlight, index) => ({
    '@type': 'oa:Annotation',
    motivation: 'sc:painting',
    resource: {
      '@type': 'cnt:ContentAsText',
      chars: highlight.text,
    },
    on: canvasId + '#xywh=' + highlight.xywh,
  })),
};

ポイントは on プロパティで、キャンバスURI#xywh=x,y,width,height の形式でハイライト領域を指定します。

3. マニフェスト読み込み完了の検知

let highlightAdded = false;
const unsubscribe = miradorViewer.store.subscribe(() => {
  if (highlightAdded) return;

  const state = miradorViewer.store.getState();
  const manifests = state.manifests || {};
  const manifest = manifests[config.manifestUrl];

  // マニフェストが読み込み完了したらハイライトを追加
  if (manifest && !manifest.isFetching && manifest.json) {
    highlightAdded = true;
    unsubscribe();
    addHighlights(miradorViewer, config.canvasId, config.highlights);
  }
});

MiradorはReduxを使用しているため、store.subscribe()でステート変更を監視できます。マニフェストのisFetchingfalseになり、jsonが存在する状態になったタイミングでハイライトを追加します。

これにより、setTimeoutを使用するよりも確実にマニフェスト読み込み完了後にハイライトを追加できます。

4. Miradorへの登録

// 検索パネルを追加
const addAction = Mirador.addCompanionWindow('window-1', {
  content: 'search',
  position: 'right',
});
viewer.store.dispatch(addAction);

// 検索結果を登録
const searchAction = Mirador.receiveSearch(
  'window-1',
  searchCompanionWindowId,
  canvasId + '/search',
  searchResponse
);
viewer.store.dispatch(searchAction);

receiveSearchアクションを使用して、構築したレスポンスをMiradorに登録します。

応用例

複数のハイライト

highlights: [
  { xywh: '100,200,300,400', text: '領域1' },
  { xywh: '500,600,200,300', text: '領域2' },
  { xywh: '800,900,150,250', text: '領域3' },
]

APIからデータを取得

async function loadHighlightsFromAPI(canvasId) {
  const response = await fetch(`/api/annotations?canvas=${encodeURIComponent(canvasId)}`);
  const data = await response.json();
  return data.annotations.map(anno => ({
    xywh: anno.coordinates,
    text: anno.text,
  }));
}

// 使用例
const highlights = await loadHighlightsFromAPI(config.canvasId);
addHighlights(miradorViewer, config.canvasId, highlights);

URLパラメータから動的に生成

このリポジトリのviewer.htmlでは、URLパラメータからハイライト情報を受け取り、動的にビューアを生成しています。

const params = new URLSearchParams(window.location.search);
const manifestUrl = params.get('manifest');
const canvasId = params.get('canvas');
const xywh = params.get('xywh');
const text = params.get('text') || 'Highlight';

座標の取得方法

ハイライト領域の座標(xywh)を取得するには、以下の方法があります:

  1. IIIF Image APIのRegion指定 - 画像編集ソフトで座標を計測
  2. OCRエンジンの出力 - Tesseract等のOCR結果に含まれる座標情報
  3. アノテーションツール - IIIF対応のアノテーション作成ツールを使用
  4. 機械学習モデルの出力 - 物体検出モデルのバウンディングボックス

まとめ

MiradorのreceiveSearchアクションを活用することで、IIIF Search APIに非対応のマニフェストでも任意の領域をハイライト表示できます。この方法は:

  • データソースに依存しない - API、ファイル、データベース等から取得したデータを利用可能
  • Search APIと同じUI - Miradorの標準的な検索結果表示機能を活用
  • 複数ハイライト対応 - 配列で複数の領域を一度に登録可能
  • 確実なタイミング制御 - store.subscribe()によりマニフェスト読み込み完了を検知

ファイル構成

mirador-highlight/
├── README.md                    # プロジェクト説明
├── LICENSE                      # MITライセンス
├── CONTRIBUTING.md              # コントリビューションガイド
├── mirador-custom-highlight.md  # このドキュメント
└── docs/                        # GitHub Pages用
    ├── index.html               # ハイライト生成フォーム
    └── viewer.html              # Miradorビューア

参考リンク

Discussion