Mirador 4でキャンバス指定と検索語ハイライトを同時に実現する方法
はじめに
IIIF(International Image Interoperability Framework)ビューアとして広く使われているMiradorで、以下の要件を満たす実装を行いました:
- URLパラメータで指定したキャンバス(ページ)を初期表示する
- 指定したキャンバス内の検索語をハイライト表示する
本記事では、この要件を実現するためのアプローチと実装方法を共有します。
アプローチの検討
defaultSearchQueryオプション
Mirador 4では、ウィンドウ設定に defaultSearchQuery オプションを指定することで、初期化時に自動的に検索を実行できます:
const miradorViewer = Mirador.viewer({
windows: [{
manifestId: manifestUrl,
canvasId: canvasId,
defaultSearchQuery: '検索語',
}],
});
このオプションは検索を自動実行する便利な機能ですが、今回の要件では以下の点を考慮する必要がありました:
-
ページ遷移の制御 - 検索実行時、最初のヒットのページに自動遷移する仕様のため、
canvasIdで指定したページに留まりたい場合は追加の制御が必要 - 検索完了タイミングの把握 - 非同期で検索が実行されるため、完了後に処理を行いたい場合はRedux状態の監視が必要
より直接的なアプローチ
検討の結果、receiveSearch アクションを直接使用するアプローチを採用しました。このアプローチでは:
- Search APIを直接呼び出して、指定キャンバスに該当するヒットのみを取得
- IIIF Search API形式のレスポンスを構築
-
receiveSearchアクションで検索結果としてMiradorに登録
これにより、指定キャンバスの表示を維持しながら、Miradorの検索ハイライト機能をそのまま活用できます。
実装
1. Search APIからヒットを取得する関数
const getSearchHitsForCanvas = async (
manifestUrl: string,
canvasId: string,
query: string
): Promise<{ id: string; chars: string; xywh: string }[]> => {
try {
// manifestからsearch serviceのURLを取得
const manifestResponse = await fetch(manifestUrl);
if (!manifestResponse.ok) return [];
const manifest = await manifestResponse.json();
// IIIF Search API serviceを探す
const services = manifest.service || [];
const searchService = (Array.isArray(services) ? services : [services]).find(
(s: { profile?: string }) => s.profile?.includes('search')
);
if (!searchService) return [];
const searchBaseUrl = searchService['@id'] || searchService.id;
const searchUrl = `${searchBaseUrl}?q=${encodeURIComponent(query)}`;
const response = await fetch(searchUrl);
if (!response.ok) return [];
const data = await response.json();
const resources = data.resources || [];
// 指定キャンバスに該当するヒットのみを抽出
const hits: { id: string; chars: string; xywh: string }[] = [];
for (const resource of resources) {
const [resourceCanvas, fragment] = resource.on.split('#');
if (resourceCanvas === canvasId && fragment) {
hits.push({
id: resource['@id'] || resource.id,
chars: resource.resource?.chars || query,
xywh: fragment.replace('xywh=', ''),
});
}
}
return hits;
} catch (error) {
console.error('Failed to fetch search hits:', error);
return [];
}
};
この実装では、manifestのserviceプロパティからIIIF Search APIのエンドポイントを動的に取得しています。これにより、任意のIIIF準拠サーバーで動作します。
2. Mirador初期化とハイライト登録
// Miradorを初期化
const miradorViewer = window.Mirador.viewer({
id: 'mirador-viewer',
windows: [{
id: 'window-1',
manifestId: manifestUrl,
canvasId: canvasId, // 指定キャンバスを表示
thumbnailNavigationPosition: 'far-right',
}],
window: {
allowFullscreen: true,
allowClose: false,
sideBarOpen: true,
},
workspaceControlPanel: {
enabled: false,
},
});
// 検索語とキャンバスが指定されている場合、ハイライトを追加
if (searchQuery && canvasId) {
const hits = await getSearchHitsForCanvas(manifestUrl, canvasId, searchQuery);
if (hits.length > 0) {
const M = window.Mirador as Record<string, unknown>;
const receiveSearch = M.receiveSearch as (
windowId: string,
companionWindowId: string,
searchId: string,
searchJson: unknown
) => Record<string, unknown>;
if (receiveSearch) {
// IIIF Search API形式のレスポンスを構築
const searchResponse = {
'@context': 'http://iiif.io/api/search/1/context.json',
'@id': `${canvasId}/search?q=${encodeURIComponent(searchQuery)}`,
'@type': 'sc:AnnotationList',
within: {
'@type': 'sc:Layer',
total: hits.length,
},
resources: hits.map((hit, index) => ({
'@id': `${canvasId}/search-result-${index}`,
'@type': 'oa:Annotation',
motivation: 'sc:painting',
resource: {
'@type': 'cnt:ContentAsText',
chars: hit.chars,
},
on: `${canvasId}#xywh=${hit.xywh}`,
})),
};
// 検索パネルを右側に追加
const addCompanionWindow = M.addCompanionWindow as (
windowId: string,
payload: { content: string; position: string }
) => Record<string, unknown>;
const addAction = addCompanionWindow('window-1', {
content: 'search',
position: 'right',
});
miradorViewer.store.dispatch(addAction);
// companionWindowIdを取得
const state = miradorViewer.store.getState();
const searchCompanionWindowId = Object.keys(state.companionWindows).find(
id => state.companionWindows[id].content === 'search'
);
if (searchCompanionWindowId) {
// 検索結果を登録
const searchAction = receiveSearch(
'window-1',
searchCompanionWindowId,
`${canvasId}/search?q=${encodeURIComponent(searchQuery)}`,
searchResponse
);
miradorViewer.store.dispatch(searchAction);
}
}
}
}
ポイント
1. Miradorのアクション関数へのアクセス
Mirador 4では、アクション関数がグローバルオブジェクトから直接アクセスできます:
const M = window.Mirador as Record<string, unknown>;
const receiveSearch = M.receiveSearch;
const addCompanionWindow = M.addCompanionWindow;
Mirador.actions ではなく、Mirador.receiveSearch のように直接アクセスする点に注意してください。
2. IIIF Search API形式のレスポンス
receiveSearch に渡すJSONは、IIIF Search API 1.0の形式に準拠する必要があります:
{
"@context": "http://iiif.io/api/search/1/context.json",
"@type": "sc:AnnotationList",
"resources": [
{
"@type": "oa:Annotation",
"motivation": "sc:painting",
"resource": {
"@type": "cnt:ContentAsText",
"chars": "ヒットしたテキスト"
},
"on": "キャンバスURI#xywh=x,y,width,height"
}
]
}
3. CompanionWindowの配置
検索パネルの配置は position パラメータで制御できます:
-
left: 左側(メタデータパネルと同じ位置) -
right: 右側
左側にメタデータ、右側に検索結果という配置が自然です。
結論
receiveSearch アクションを直接呼び出すことで、以下のメリットが得られました:
- キャンバス表示の制御 - 指定したキャンバスを初期表示しながらハイライトを追加できる
- ハイライトの即時表示 - 検索結果を直接登録するため、ハイライトが即座に表示される
- パネル配置の制御 - 検索パネルの位置を自由に設定できる
この方法はMiradorのReduxストアの仕組みを活用しており、柔軟なカスタマイズが可能です。
Discussion