IIIFとNFTの統合
IIIF(International Image Interoperability Framework)は、デジタル文化財の画像と関連メタデータを標準的な方法で公開・共有するための国際規格です。世界中の図書館・博物館・美術館がIIIFを採用しており、高解像度画像のズーム表示、異なる機関の画像の比較、注釈の付与などが可能です。
本章では、IIIFのManifest URIをNFTのtokenURIとしてオンチェーンに記録し、Mirador等のIIIFビューアで閲覧できるデジタルアーカイブNFTを構築します。これにより、ブロックチェーンの不変性とIIIFの相互運用性を組み合わせた、デジタル文化財管理の仕組みを実現します。
IIIFの基本概念
| コンポーネント | 役割 |
|---|---|
| Image API | 画像の切り出し・サイズ変更・回転等を行うAPI |
| Presentation API | 画像の構造やメタデータを記述するManifest |
| Canvas | Manifestの基本単位。1つの画像面を表す |
| Manifest | 複数のCanvasをまとめた単位。1つの資料を表す |
IIIF対応NFTコントラクト
通常のNFTでは tokenURI にIPFS上のJSONメタデータを指定しますが、ここではIIIF Manifest URIを直接保持するフィールドを追加します。tokenURIとは別にIIIF固有の情報を管理することで、IIIFビューアとNFTマーケットプレイスの両方に対応できます。
// contracts/IIIFCulturalNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract IIIFCulturalNFT is ERC721, ERC721URIStorage, Ownable {
uint256 private _nextTokenId;
/// @notice IIIF関連の情報
struct IIIFInfo {
string manifestURI; // IIIF Manifest URI
string canvasId; // 対象の Canvas ID
string label; // 資料名(ラベル)
}
// tokenId => IIIFInfo
mapping(uint256 => IIIFInfo) private _iiifInfo;
// イベント
event NFTMintedWithIIIF(
uint256 indexed tokenId,
address indexed to,
string manifestURI,
string canvasId
);
event IIIFInfoUpdated(
uint256 indexed tokenId,
string manifestURI,
string canvasId
);
constructor()
ERC721("IIIF Cultural Heritage NFT", "IIIF-CHN")
Ownable(msg.sender)
{}
/// @notice IIIF情報付きでNFTをミントする
/// @param to ミント先アドレス
/// @param nftMetadataURI NFT標準メタデータのURI(ipfs://...)
/// @param manifestURI IIIF Manifest URI
/// @param canvasId 対象の Canvas ID
/// @param label 資料名
function mintWithIIIF(
address to,
string memory nftMetadataURI,
string memory manifestURI,
string memory canvasId,
string memory label
) public onlyOwner returns (uint256) {
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, nftMetadataURI);
_iiifInfo[tokenId] = IIIFInfo({
manifestURI: manifestURI,
canvasId: canvasId,
label: label
});
emit NFTMintedWithIIIF(tokenId, to, manifestURI, canvasId);
return tokenId;
}
/// @notice IIIF情報を更新する(オーナーのみ)
function updateIIIFInfo(
uint256 tokenId,
string memory manifestURI,
string memory canvasId,
string memory label
) public onlyOwner {
require(_ownerOf(tokenId) != address(0), "Token does not exist");
_iiifInfo[tokenId] = IIIFInfo({
manifestURI: manifestURI,
canvasId: canvasId,
label: label
});
emit IIIFInfoUpdated(tokenId, manifestURI, canvasId);
}
/// @notice IIIF Manifest URIを取得する
function iiifManifestOf(
uint256 tokenId
) public view returns (string memory) {
require(_ownerOf(tokenId) != address(0), "Token does not exist");
return _iiifInfo[tokenId].manifestURI;
}
/// @notice IIIF Canvas IDを取得する
function iiifCanvasOf(
uint256 tokenId
) public view returns (string memory) {
require(_ownerOf(tokenId) != address(0), "Token does not exist");
return _iiifInfo[tokenId].canvasId;
}
/// @notice IIIF情報をすべて取得する
function getIIIFInfo(
uint256 tokenId
) public view returns (IIIFInfo memory) {
require(_ownerOf(tokenId) != address(0), "Token does not exist");
return _iiifInfo[tokenId];
}
// === 必須のオーバーライド ===
function tokenURI(
uint256 tokenId
)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
function supportsInterface(
bytes4 interfaceId
)
public
view
override(ERC721, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
NFTメタデータにIIIF情報を含める
NFT標準のメタデータJSONに、IIIF関連の情報を追加します。external_url にIIIFビューアのURLを設定することで、OpenSea等のマーケットプレイスからIIIFビューアに遷移できます。
{
"name": "洛中洛外図屏風(右隻)",
"description": "国宝・洛中洛外図屏風の右隻デジタル複製。狩野永徳筆。室町時代末期の京都を描いた六曲一双の屏風。",
"image": "ipfs://QmYourThumbnailHash",
"external_url": "https://projectmirador.org/embed/?iiif-content=https://example.org/iiif/rakuchu/manifest.json",
"attributes": [
{
"trait_type": "Institution",
"value": "米沢市上杉博物館"
},
{
"trait_type": "Creator",
"value": "狩野永徳"
},
{
"trait_type": "Period",
"value": "室町時代末期"
},
{
"trait_type": "IIIF Manifest",
"value": "https://example.org/iiif/rakuchu/manifest.json"
}
]
}
テスト
// test/IIIFCulturalNFT.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
describe("IIIFCulturalNFT", function () {
const MANIFEST_URI =
"https://example.org/iiif/choju-giga/manifest.json";
const CANVAS_ID =
"https://example.org/iiif/choju-giga/canvas/p1";
const LABEL = "鳥獣戯画 甲巻";
async function deployFixture() {
const [owner, user1] = await ethers.getSigners();
const NFT = await ethers.getContractFactory("IIIFCulturalNFT");
const nft = await NFT.deploy();
return { nft, owner, user1 };
}
it("IIIF情報付きでNFTをミントできる", async function () {
const { nft, owner } = await deployFixture();
await nft.mintWithIIIF(
owner.address,
"ipfs://QmMetadata",
MANIFEST_URI,
CANVAS_ID,
LABEL
);
// NFT標準メタデータ
expect(await nft.tokenURI(0)).to.equal("ipfs://QmMetadata");
// IIIF情報
expect(await nft.iiifManifestOf(0)).to.equal(MANIFEST_URI);
expect(await nft.iiifCanvasOf(0)).to.equal(CANVAS_ID);
const info = await nft.getIIIFInfo(0);
expect(info.label).to.equal(LABEL);
});
it("NFTMintedWithIIIFイベントが発行される", async function () {
const { nft, owner } = await deployFixture();
await expect(
nft.mintWithIIIF(
owner.address,
"ipfs://QmMetadata",
MANIFEST_URI,
CANVAS_ID,
LABEL
)
)
.to.emit(nft, "NFTMintedWithIIIF")
.withArgs(0, owner.address, MANIFEST_URI, CANVAS_ID);
});
it("IIIF情報を更新できる", async function () {
const { nft, owner } = await deployFixture();
await nft.mintWithIIIF(
owner.address,
"ipfs://QmMetadata",
MANIFEST_URI,
CANVAS_ID,
LABEL
);
const newManifest =
"https://example.org/iiif/v2/choju-giga/manifest.json";
await nft.updateIIIFInfo(0, newManifest, CANVAS_ID, LABEL);
expect(await nft.iiifManifestOf(0)).to.equal(newManifest);
});
it("オーナー以外はIIIF情報を更新できない", async function () {
const { nft, owner, user1 } = await deployFixture();
await nft.mintWithIIIF(
owner.address,
"ipfs://QmMetadata",
MANIFEST_URI,
CANVAS_ID,
LABEL
);
await expect(
nft
.connect(user1)
.updateIIIFInfo(0, "https://evil.com/manifest", "", "")
).to.be.revertedWithCustomError(nft, "OwnableUnauthorizedAccount");
});
});
フロントエンドでIIIFビューアを統合する
NFTの詳細ページにMiradorビューアを埋め込み、IIIF Manifestをブラウザ上で表示します。
// src/components/IIIFViewer.tsx
"use client";
import { useEffect, useRef } from "react";
import { useReadContract } from "wagmi";
// IIIFCulturalNFTのABI(getIIIFInfo関数のみ)
const IIIF_NFT_ABI = [
{
inputs: [{ name: "tokenId", type: "uint256" }],
name: "getIIIFInfo",
outputs: [{
components: [
{ name: "manifestURI", type: "string" },
{ name: "canvasId", type: "string" },
{ name: "label", type: "string" },
],
type: "tuple",
}],
stateMutability: "view",
type: "function",
},
] as const;
const NFT_ADDRESS = process.env
.NEXT_PUBLIC_IIIF_NFT_ADDRESS as `0x${string}`;
interface IIIFViewerProps {
tokenId: number;
}
export function IIIFViewer({ tokenId }: IIIFViewerProps) {
const viewerRef = useRef<HTMLDivElement>(null);
const { data: iiifInfo } = useReadContract({
address: NFT_ADDRESS,
abi: IIIF_NFT_ABI,
functionName: "getIIIFInfo",
args: [BigInt(tokenId)],
});
useEffect(() => {
if (!iiifInfo?.manifestURI || !viewerRef.current) return;
// Miradorを動的にロード(Next.jsのSSR対策)
import("mirador").then((Mirador) => {
Mirador.default({
id: viewerRef.current!.id,
windows: [
{
manifestId: iiifInfo.manifestURI,
canvasId: iiifInfo.canvasId || undefined,
},
],
window: {
allowFullscreen: true,
allowMaximize: false,
},
});
});
}, [iiifInfo]);
if (!iiifInfo) {
return <p className="text-gray-400">IIIF情報を読み込み中...</p>;
}
return (
<div>
<h3 className="text-lg font-semibold mb-2">
{iiifInfo.label}
</h3>
<p className="text-sm text-gray-400 mb-4">
Manifest: {iiifInfo.manifestURI}
</p>
<div
id={`mirador-viewer-${tokenId}`}
ref={viewerRef}
className="w-full h-[500px] bg-black rounded-lg"
/>
</div>
);
}
Miradorの代わりにOpenSeadragonを使う場合は、Image APIのエンドポイントを直接指定します。
// OpenSeadragonを使う場合の例
import OpenSeadragon from "openseadragon";
useEffect(() => {
if (!iiifInfo?.manifestURI) return;
// ManifestからImage API URLを取得してビューアに設定
fetch(iiifInfo.manifestURI)
.then((res) => res.json())
.then((manifest) => {
// IIIF Presentation API 3.0の場合
const canvas = manifest.items?.[0];
const imageService =
canvas?.items?.[0]?.items?.[0]?.body?.service?.[0];
if (imageService) {
OpenSeadragon({
element: viewerRef.current!,
tileSources: {
"@context":
"http://iiif.io/api/image/3/context.json",
id: imageService.id,
type: "ImageService3",
profile: "level2",
},
showNavigator: true,
});
}
});
}, [iiifInfo]);
IPFSにIIIF Manifestを保存する
IIIFマニフェスト自体をIPFSに保存することで、マニフェストの永続性も確保できます。
// scripts/uploadIIIFManifest.ts
import PinataSDK from "@pinata/sdk";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: process.env.PINATA_GATEWAY!,
});
async function uploadManifestToIPFS() {
// IIIF Presentation API 3.0 形式のManifest
const manifest = {
"@context": "http://iiif.io/api/presentation/3/context.json",
id: "https://example.org/iiif/choju-giga/manifest.json",
type: "Manifest",
label: { ja: ["鳥獣戯画 甲巻"] },
metadata: [
{
label: { ja: ["所蔵"] },
value: { ja: ["高山寺"] },
},
{
label: { ja: ["時代"] },
value: { ja: ["平安時代後期"] },
},
],
items: [
{
id: "https://example.org/iiif/choju-giga/canvas/p1",
type: "Canvas",
label: { ja: ["第1場面"] },
height: 3000,
width: 8000,
items: [
{
id: "https://example.org/iiif/choju-giga/page/p1/1",
type: "AnnotationPage",
items: [
{
id: "https://example.org/iiif/choju-giga/annotation/p1-image",
type: "Annotation",
motivation: "painting",
body: {
id: "https://example.org/iiif/choju-giga/full/max/0/default.jpg",
type: "Image",
format: "image/jpeg",
service: [
{
id: "https://example.org/iiif/choju-giga",
type: "ImageService3",
profile: "level2",
},
],
},
target:
"https://example.org/iiif/choju-giga/canvas/p1",
},
],
},
],
},
],
};
// IPFSにアップロード
const result = await pinata.pinJSONToIPFS(manifest, {
pinataMetadata: { name: "IIIF-Manifest-ChojuGiga" },
});
console.log("IIIF Manifest URI:", `ipfs://${result.IpfsHash}`);
console.log(
"HTTP URL:",
`https://gateway.pinata.cloud/ipfs/${result.IpfsHash}`
);
return `ipfs://${result.IpfsHash}`;
}
uploadManifestToIPFS().catch(console.error);
まとめ
本章では、IIIFとNFTを統合したデジタルアーカイブシステムを構築しました。IIIF Manifest URIをオンチェーンに記録することで、ブロックチェーンの不変性(改ざん耐性)とIIIFの相互運用性(標準化されたビューア対応)を両立させています。
この仕組みにより、文化財のデジタル画像の所有権・来歴をブロックチェーンで管理しつつ、MiradorやOpenSeadragonなどのIIIFビューアで高品質な閲覧体験を提供できます。
次章では、デジタル保存の国際標準であるOAIS参照モデルとBagIt仕様に基づく、AIPパッケージの生成について学びます。