ピラミッドTIFFの作成において、ImageMagickがうまく動作しないケースがある?

概要 IIIFの画像配信に向けたピラミッドTIFFの作成において、ImageMagickがうまく動作しないケースがあり、調査してみました。 参考 以下のようなページで、変換方法が説明されています。 https://samvera.github.io/serverless-iiif/docs/source-images#creating-tiled-tiffs Using the VIPS command line # For a 3-channel source image vips tiffsave source_image.tif output_image.tif --tile --pyramid --compression jpeg --tile-width 256 --tile-height 256 # For a source image with an alpha channel vips extract_band source_image.tif temp_image.v 0 --n 3 \ && vips tiffsave temp_image.v output_image.tif --tile --pyramid --compression jpeg --tile-width 256 --tile-height 256 \ && rm temp_image.v Using ImageMagick convert source_image.tif -alpha off \ -define tiff:tile-geometry=256x256 \ -define tiff:generate-pyramids=true \ -compress jpeg \ 'ptif:output_image.tif' 対象データ 以下の画像を使用させていただきます。 ...

2025年3月11日 · 3 分 · Nakamura

ArchivesSpaceをDockerで起動する

概要 ArchivesSpaceをDockerで起動する機会がありましたので、備忘録です。 方法 以下に記載されています。 https://docs.archivesspace.org/administration/docker/ 以下のようにcloneした上で、コンテナを起動します。 git clone https://github.com/archivesspace/archivesspace cd archivesspace docker compose up --detach ドキュメントでは、以下のように記載されていますが、Public User interfaceは3001ポート、Staff User Interfaceは3000ポート、およびバックエンドシステムには4567ポートでアクセスできました。 Using the default proxy configuration, the Public User interface becomes available at http://localhost/ and the Staff User Interface at: http://localhost/staff/ (default login with: admin / admin) 翻訳ファイルの更新 Public User interfaceはpublic/config/locales/ja.yml、Staff User Interfaceはfrontend/config/locales/ja.ymlの翻訳ファイルを更新することで、翻訳データを適用することができました。 以下、翻訳ファイルの更新前後の画面比較です。 まとめ ArchivesSpaceの利用にあたり、参考になりましたら幸いです。

2025年3月11日 · 1 分 · Nakamura

Omeka Sのファイルをmdx.jpのオブジェクトストレージに保存する

概要 Omeka Sのファイルをmdx.jpのオブジェクトストレージに保存する方法に関する備忘録です。 ベースとするモジュール Amazon S3との連携を可能にする以下のモジュールをベースとします。 https://omeka.org/s/modules/AmazonS3/ 本モジュールでは、Omeka Sで取り扱う画像や動画といったメディアのファイルをAmazon S3に保存するための拡張機能を提供します。 一方、endpointの指定ができないため、mdx.jpのオブジェクトストレージなどを対象にすることはできませんでした。 モジュールのカスタマイズ 上述した背景を踏まえて、Amazon S3以外のオブジェクトストレージを利用できるように、モジュールをカスタマイズしました。カスタマイズした結果は、以下のリポジトリで公開しています。 https://github.com/nakamura196/Omeka-S-module-AmazonS3 なお、カスタマイズについては、エディタとしてCursorを使用し、s3互換のオブジェクトストレージにも対応したいという依頼をclaude-3.7-sonnetに提出し、その結果を反映しています。 結果、上記のモジュールを使用することにより、Omeka Sで登録したメディアが以下のようなURLでアクセス可能になりました。 https://s3ds.mdx.jp/<バケット名>/large/3e0a78e1cbc239f37cfff0e777c40c2f9b2f5c92.jpg 以下は、filesディレクトリを、mdx.jpに接続したCyberduckで表示した例です。 モジュールの設定内容は以下のとおりです。カスタムエンドポイントURLという項目が追加されており、https://s3ds.mdx.jpを指定することで、mdx.jpのオブジェクトストレージを利用できるようになりました。 なお、上記の画面キャプチャで表示されているとおり、mdx.jpのオブジェクトストレージにファイルが保存される設定をしても、現時点ではWrong region. Please use region of a bucket:と表示されてしまいます。この点は、今後修正予定です。 モジュールのインストール 今回フォークして作成したカスタムモジュールをインストールするには、以下の手順を踏む必要があります。 cd <モジュールが格納されているディレクトリ> git clone https://github.com/nakamura196/Omeka-S-module-AmazonS3 AmazonS3 cd AmazonS3 composer install --no-dev Omeka Sにおいて、ソースからモジュールを使用するには、おおよそ共通して上記のような手続きが必要になります。 参考 Omeka Sのモジュールにおいて、同様の機能を提供するものとして、Any Cloudがあります。 https://github.com/HBLL-Collection-Development/omeka-s-any-cloud こちらもAmazon S3との接続機能を提供しており、またカスタイズを行う必要がなく、AWS Endpointを入力する項目が提供されていました。 ただ、これらの項目に先述したmdx.jpのオブジェクトストレージの情報を入力したところ、アイテムなどを登録する画面で以下のエラーが表示されました。 原因や対処方法については引き続き調査したいと思いますが、このエラーが遭遇したため、Any Cloudではなく、Amazon S3モジュールをカスタマイズする選択を行いました。 まとめ 2025年度からmdx.jpのオブジェクトストレージは無料で使用可能になるということで、デジタルアーカイブにおける公開画像の格納先や、また長期保存のためのストレージとしても有効な選択肢になるかと思います。 https://mdx.jp/mdx1/news/4839 デジタルアーカイブの構築や活用にあたり、参考になりましたら幸いです。

2025年3月7日 · 1 分 · Nakamura

mdx.jpのオブジェクトストレージとIIP Image(IIIF Image Server)を使ってIIIF画像を配信する

概要 mdx.jpのオブジェクトストレージとIIP Image(IIIF Image Server)を使ってIIIF画像を配信する試行の備忘録です。 以下の記事の続きです。 Docker版IIP Image 以下で、IIPImage serverのDocker Imageが公開されていましたので、こちらを使います。 https://hub.docker.com/r/iipsrv/iipsrv 以下の記事などを参考に、Dockerをインストールします。 https://qiita.com/Marron-chan/items/570c7c7baaae3b4d6b11 実行 前回の記事に倣い、以下のようにmdx.jpのオブジェクトストレージをマウントします。 s3fs satoru196 ~/s3mount -o passwd_file=~/.passwd-s3fs -o url=https://s3ds.mdx.jp -o use_path_request_style -o allow_other 注意点として、前回の記事から、-o allow_otherを追加しています。これを追加しないと、次のコンテナ起動時に以下のエラーが発生しました。 docker: Error response from daemon: error while creating mount source path '~/s3mount/iip/images': mkdir ~/s3mount: file exists Run 'docker run --help' for more information -o allow_otherオプションを追加した上で、以下を実行します。無事に起動しました。 docker run -it -p 9000:9000 -p 8080:80 -v ~/s3mount/iip/images/:/images --rm iipsrv/iipsrv <-----------------------------------> Thu Mar 6 22:35:43 2025 IIPImage Server. Version 1.2 *** Ruven Pillay <ruven@users.sourceforge.net> *** Verbosity level set to 5 Running in standalone mode on socket: 0.0.0.0:9000 with backlog: 2048 Setting maximum image cache size to 10MB Setting filesystem prefix to '/images/' Setting filesystem suffix to '' Setting default JPEG quality to 75 Setting default PNG compression level to 1 Setting default WebP compression level to 50 Setting maximum CVT size to 5000 Setting HTTP Cache-Control header to 'max-age=86400' Setting 3D file sequence name pattern to '_pyr_' Setting default IIIF Image API version to 3 Setting Allow Upscaling to true Setting ICC profile embedding to true Setting up JPEG2000 support via OpenJPEG Setting image processing engine to CPU processor OpenMP enabled for parallelized image processing with 2 threads Setting URI mapping to iiif=>IIIF. Supported protocol: IIIF Memcached support enabled. Connected to servers: 'localhost' with timeout 86400 Initialisation Complete. <-----------------------------------> そして、今回の設定では、オブジェクトストレージの/satoru196/iip/imagesにtiled multi-resolution pyramid TIFFファイルを格納し、以下のようなURLでアクセスできることを確認します。 ...

2025年3月7日 · 2 分 · Nakamura

s3fs を使用してmdx.jpのオブジェクトストレージをファイルシステムのようにマウントする方法

概要 s3fs を使用してmdx.jpのオブジェクトストレージをファイルシステムのようにマウントする機会がありましたので、備忘録です。 1. 事前準備 Ubuntu を対象とします。 ✅ s3fs のインストール sudo apt update sudo apt install s3fs ✅ 認証情報の設定 mdx.jpのオブジェクトストレージの アクセスキー と シークレットキー を ~/.passwd-s3fs に保存。 echo “ACCESS_KEY:SECRET_KEY” > ~/.passwd-s3fs chmod 600 ~/.passwd-s3fs # セキュリティのため権限変更 2. S3 ストレージをローカルにマウント ✅ マウントポイントを作成 mkdir ~/s3mount ✅ s3fs でマウント s3fs your-bucket /s3mount -o passwd_file=/.passwd-s3fs -o url=https://s3ds.mdx.jp -o use_path_request_style オプションの説明 • -o passwd_file=~/.passwd-s3fs → 認証情報を指定 • -o url=https://s3ds.mdx.jp → オブジェクトストレージのエンドポイント • -o use_path_request_style → MinIO や Ceph のような “パススタイル” の S3 互換ストレージで必要 ...

2025年3月6日 · 1 分 · Nakamura

Algolia における「a に x を含み、b に y を含む」部分一致検索の調査

この記事は、AIが作成し、一部を人が修正したものです。 はじめに フルテキスト検索エンジンの中でも、Typesense、MeiliSearch、Algolia は小規模なプロジェクト向けの選択肢として注目されています。しかし、「a に x を含み、b に y を含む」部分一致検索 が可能かどうかは、プロジェクトの要件に関わる重要なポイントです。本記事では、Algolia での部分一致検索の可否や、Elasticsearch との比較を行います。 Algolia での部分一致検索 Algolia では、全文検索 (query) を利用できますが、特定のフィールドごとに部分一致検索を行うには制限があります。 方法1:query を使った検索(部分一致可能だがフィールド指定不可) index.search('x y') 特徴: x や y を含むデータを全フィールドから検索。 どのフィールドでヒットしたかを制限できない 。 方法2:restrictSearchableAttributes を使う(単一フィールドの検索) index.search('x', { restrictSearchableAttributes: ['a'] }); index.search('y', { restrictSearchableAttributes: ['b'] }); 特徴: a に x を含むデータ、b に y を含むデータを個別に検索可能。 両方の条件を同時に適用する方法はない 。 Algolia の結論 ✅ 部分一致検索は可能だが、複数フィールドの AND 条件は難しい 。 ❌ 「a に x を含み、b に y を含む」検索は標準では不可 。 Elasticsearch での部分一致検索 Elasticsearch では、bool クエリを使うことで「a に x を含み、b に y を含む」部分一致検索が可能です。 ...

2025年3月3日 · 1 分 · Nakamura

WordファイルをTEI XMLに変換する方法:TEIgarage APIの活用ガイ

この記事は、AIが作成し、一部を人が修正したものです。 はじめに デジタル人文学の世界では、文書をTEI(Text Encoding Initiative)形式で保存することが一般的になっています。TEIは学術的なテキストを構造化するための標準規格です。今回は、Microsoft Wordで作成した文書をTEI XML形式に変換する方法を、Pythonを使って解説します。 TEIgarageとは? TEIgarageは、さまざまな形式の文書をTEI XMLに変換するためのオンラインサービスです。このサービスはAPIを提供しており、プログラムから直接利用することができます。今回はこのAPIをPythonから呼び出して、Wordファイルを変換してみましょう。 必要なもの Python 3.6以上 requestsライブラリ(APIリクエスト用) インターネット接続 変換したいWordファイル(.docx形式) 手順 1. 必要なライブラリをインストールする まず、必要なライブラリをインストールしましょう。コマンドプロンプトやターミナルで以下のコマンドを実行します。 pip install requests 2. Pythonスクリプトを作成する 次に、以下のPythonコードをword_to_tei.pyなどの名前で保存します。 import requests import os import zipfile from io import BytesIO def convert_docx_to_tei_xml(file_path, output_path): # OxGarageのエンドポイント input_document_type = "docx%3Aapplication%3Avnd.openxmlformats-officedocument.wordprocessingml.document" output_document_type = "TEI%3Atext%3Axml" TEIGARAGE_URL = f"https://teigarage.tei-c.org/ege-webservice/Conversions/{input_document_type}/{output_document_type}/" # .docx ファイルを開いてAPIに送信 with open(file_path, "rb") as file: files = {"file": file} response = requests.post(TEIGARAGE_URL, files=files) # 変換結果をファイルとして保存せずに展開 if response.status_code == 200: # zipファイルをメモリ上で展開 with zipfile.ZipFile(BytesIO(response.content)) as zip_ref: # tei.xmlファイルを保存 for member in zip_ref.namelist(): if member.endswith("tei.xml"): zip_ref.extract(member, os.path.dirname(output_path)) tei_xml_path = os.path.join(os.path.dirname(output_path), member) os.rename(tei_xml_path, output_path) print("TEI/XML 変換成功!tei.xml に保存されました。") break else: print("エラー: tei.xml ファイルが見つかりません。") else: print("エラー:", response.status_code, response.text) # メイン処理 if __name__ == "__main__": # 変換したいWordファイルのパスを指定 word_file = "documents/sample.docx" # ここを実際のファイルパスに変更してください # 出力ファイルのパスを指定 output_file = "output/sample_tei.xml" # 出力先を指定 try: # Wordファイルを変換 convert_docx_to_tei_xml(word_file, output_file) except Exception as e: print(f"エラーが発生しました: {e}") 3. スクリプトを実行する スクリプト内のword_file変数を、変換したいWordファイルの実際のパスに変更します。同様に、output_file変数も希望の出力先に変更してください。 ...

2025年3月3日 · 1 分 · Nakamura

DrupalのSortableviewsモジュールを使って、コンテンツを並び替えて、その結果をフィールドに保存する

概要 DrupalのSortableviewsモジュールを使って、コンテンツを並び替えて、その結果をフィールドに保存する方法について紹介します。 https://www.drupal.org/project/sortableviews 以下のように説明されています。 This one is similar to Draggableview module except it can save the position value ( After drag and drop the question ) into custom content type field. これは DraggableViews モジュールと似ています が、ドラッグ&ドロップで並び替えた後の 位置情報をカスタムコンテンツタイプのフィールドに保存できる 点が異なります。 インストール 通常の方法でインストールできました。 コンテンツタイプの作成 ソート対象のコンテンツタイプを作成します。ここでは、teamというコンテンツタイプを対象にします。 そして並び順の重みを保存するためのweightフィールドを作成しました。 Viewsの作成 作成したViewsは以下です。 まず、フォーマットを「Sortable table」にします。この時、field to use for weightの項目で、重みを保存したいフィールドを選択します。 次に、フィールドで「Sortableviews: Drag and drop …」を追加します。これにより、以下のようにドラッグ&ドロップのためのアイコンが表示されます。 さらに、並び替え基準として、「weight(昇順)」に設定しておきます。 最後、ヘッダーの部分で、「Save Sortableviews changes」を追加します。これにより、並び替え後に、「変更を保存」ボタンが表示されます。 APIからの利用 今回の設定では、ソート結果はfield_weightに保存されるため、例えば以下のようにAPIから利用できます。 /jsonapi/node/team?sort=field_weight 昇順に並び替えた結果を取得できます。これにより、decoupledな構成でもソート結果を利用することができます。 ...

2025年3月2日 · 1 分 · Nakamura

Next.js + CETEIcean + React TEI Routerを使ったビューア開発

概要 Next.js、CETEIcean、React TEI Routerを組み合わせたTEI/XMLビューアの開発についての備忘録です。 背景 CETEIceanは、TEI/XML を HTML5 に変換する JavaScript ライブラリです。 https://github.com/TEIC/CETEIcean そして、React TEI Routerは、CETEIcean をベースに React コンポーネントで TEI/XML を構造化して表示できるライブラリです。以下のように説明されています。 https://github.com/pfefferniels/react-teirouter TEI for React using CETEIcean and routes これらを組み合わせることで、Next.js において TEI/XML をカスタマイズして表示できるビューア を作成しました。 リポジトリ 以下がサンプルリポジトリです。 https://github.com/nakamura196/next-ceteicean-router 実際に動作するデモも用意しています。 https://next-ceteicean-router.vercel.app/ 実装 Next.js のページコンポーネント (page.tsx) CETEIcean を利用して XML を変換し、カスタムコンポーネントで描画します。 import React from "react"; import Render from "@/components/tei"; export default function App() { const xmlContent = `<?xml version="1.0" encoding="UTF-8"?> <TEI xmlns="http://www.tei-c.org/ns/1.0"> <text> <body> <div type="original"> 私の名前は<persName corresp="#id1">田中太郎</persName>です。 </div> <div> <p style="color: green;">こんにちは</p> <p style="color: green;">こんばんは <seg style="color: blue;">xxx</seg> </p> </div> </body> </text> </TEI>`; return <Render xmlContent={xmlContent} />; } TEIレンダリングコンポーネント CETEIcean を使って XML を HTML5 に変換。 TEIRender + TEIRoute を使い、TEI 要素ごとにカスタムコンポーネントを適用。 import { TEIRender, TEIRoute } from "react-teirouter";を使用した上で、要素毎にコンポーネントを用意しています。 ...

2025年3月2日 · 2 分 · Nakamura

Next.js for Drupal の BASE_PATH 問題と修正方法(patch-package活用)

概要 Next.js for Drupalのv2.0.0が2025/2/11にリリースされました。 https://next-drupal.org/ https://next-drupal.org/blog/next-drupal-2-0 早速試してみたところ、BASE_PATHの取り扱いについて対応が必要だったので、備忘録です。 環境変数 環境変数のサンプルは以下のようになっています。 # See https://next-drupal.org/docs/environment-variables # Required NEXT_PUBLIC_DRUPAL_BASE_URL=https://site.example.com NEXT_IMAGE_DOMAIN=site.example.com # Authentication DRUPAL_CLIENT_ID=Retrieve this from /admin/config/services/consumer DRUPAL_CLIENT_SECRET=Retrieve this from /admin/config/services/consumer # Required for On-demand Revalidation DRUPAL_REVALIDATE_SECRET=Retrieve this from /admin/config/services/next この時、NEXT_PUBLIC_DRUPAL_BASE_URLにhttps://site.example.com/xxxのようなベースパスを含めた形で指定すると、APIのリクエストはhttps://site.example.com/jsonapi/などに送られ、リソースを正しく取得できませんでした。 原因 エラーが発生しているgetResourceCollectionを確認したところ、問い合わせ先のURLを作成するbuildUrl関数において、new URL(path, this.baseUrl);が使用されていました。 ... buildUrl(path, searchParams) { const url = new URL(path, this.baseUrl); const search = ( // Handle DrupalJsonApiParams objects. searchParams && typeof searchParams === "object" && "getQueryObject" in searchParams ? searchParams.getQueryObject() : searchParams ); if (search) { url.search = stringify(search); } return url; } ... async buildEndpoint({ locale = "", path = "", searchParams } = {}) { const localeSegment = locale ? `/${locale}` : ""; if (path && !path.startsWith("/")) { path = `/${path}`; } return this.buildUrl( `${localeSegment}${this.apiPrefix}${path}`, searchParams ).toString(); } ... async getResourceCollection(type, options) { options = { withAuth: this.withAuth, deserialize: true, ...options }; const endpoint = await this.buildEndpoint({ locale: options?.locale !== options?.defaultLocale ? options.locale : void 0, resourceType: type, searchParams: options?.params }); this.debug(`Fetching resource collection of type ${type}.`); const response = await this.fetch(endpoint, { withAuth: options.withAuth, next: options.next, cache: options.cache }); await this.throwIfJsonErrors( response, "Error while fetching resource collection: " ); const json = await response.json(); return options.deserialize ? this.deserialize(json) : json; } ChatGPTに質問したところ、以下の回答が得られました。 ...

2025年3月2日 · 2 分 · Nakamura

DrupalのSearch API Algoliaモジュールを試す

概要 DrupalのSearch API Algoliaモジュールを試す機会がありましたので、備忘録です。 https://www.drupal.org/project/search_api_algolia インストール Drupal 11では、以下でインストールできました。 composer require 'drupal/search_api_algolia:^3.1' 設定 モジュールのインストール後、サーバとインデックスの設定を行います。 Add server Algoliaの設定画面で確認できる設定情報に基づき、サーバを作成します。ここでは、Write API Keyを使用しました。 Add index インデックスの作成以降は、他のモジュールでの設定と同様です。以下は、Amazon OpenSearch Serviceを使用する例です。 設定後、インデクシングを行います。 結果 以下のように、Drupalへのコンテンツ登録や更新に応じて、Algoliaのインデックスも更新されました。 まとめ このような連携により、Algoliaを用いた高速および柔軟な検索を行うことが可能になりました。 Drupalの活用にあたり、参考になりましたら幸いです。

2025年3月1日 · 1 分 · Nakamura

DrupalのJSON:APIを用いて、ユーザ名とパスワードでデータ登録を行う

概要 過去に、DrupalのJSON:APIを用いて、Pythonによるデータ登録を行う記事を執筆しました。 以下は、Basic認証を用いた方法です。 また以下は、API Keyを用いた方法です。 これらに加えて、通常のログインによる登録を行うことができたので、備忘録です。 コード 以下の通りです。ログインやCSRFトークンを取得した上で、コンテンツを登録します。 import requests import json import os from dotenv import load_dotenv class ApiClient: def __init__(self): load_dotenv(override=True) # DrupalサイトのURL(例) self.DRUPAL_BASE_URL = os.getenv("DRUPAL_BASE_URL") # エンドポイント(JSON:API) # self.JSONAPI_ENDPOINT = f"{self.DRUPAL_BASE_URL}/jsonapi/node/article" # 認証情報(Basic認証) self.USERNAME = os.getenv("USERNAME") self.PASSWORD = os.getenv("PASSWORD") def login(self): # ログインリクエスト login_url = f"{self.DRUPAL_BASE_URL}/user/login?_format=json" login_response = requests.post( login_url, json={"name": self.USERNAME, "pass": self.PASSWORD}, headers={"Content-Type": "application/json"} ) if login_response.status_code == 200: self.session_cookies = login_response.cookies def get_csrf_token(self): # CSRFトークンを取得 csrf_token_response = requests.get( f"{self.DRUPAL_BASE_URL}/session/token", cookies=self.session_cookies # ここでログインセッションを渡す ) if csrf_token_response.status_code == 200: # return csrf_token_response.text # self.csrf_token = csrf_token_response.text self.headers = { "Content-Type": "application/vnd.api+json", "Accept": "application/vnd.api+json", "X-CSRF-Token": csrf_token_response.text, } else: # raise Exception(f"CSRFトークン取得失敗: {csrf_token_response.status_code} {csrf_token_response.text}") self.csrf_token = None def create_content(self, data: dict): # 記事作成リクエスト url = f"{self.DRUPAL_BASE_URL}/jsonapi/{data['data']['type'].replace('--', '/')}" response = requests.post( # self.JSONAPI_ENDPOINT, url, headers=self.headers, cookies=self.session_cookies, json=data ) if response.status_code == 201: print("コンテンツが作成されました!") else: print("エラー:", response.status_code, response.text) これにより、以下で、コンテンツを登録することができました。 ...

2025年3月1日 · 1 分 · Nakamura

Next.jsで多言語対応の静的サイトを構築する

はじめに この記事は、GPT-4oによって生成された内容です。Next.jsを使用して多言語対応の静的サイトを構築する方法について説明します。特に、メイン言語にはURLプレフィックスを付けず、その他の言語にはプレフィックスを付ける設定に焦点を当てます。GitHub Pagesを使用してデプロイする設定も含まれています。 プロジェクトのセットアップ まず、Next.jsのプロジェクトを作成します。create-next-appを使用してプロジェクトを初期化します。 npx create-next-app@latest next-intl-ssg 必要なパッケージのインストール 多言語対応のために、next-intlをインストールします。 npm install next-intl プロジェクト構成 プロジェクトのディレクトリ構成は以下の通りです。 src/app/[locale]/about/page.tsx src/app/about/page.tsx src/lib/i18n.ts src/components/I18nProvider.tsx src/i18n/ja.json src/i18n/en.json 多言語対応の実装 next-intlを使用して、言語ごとのメッセージを管理します。src/lib/i18n.tsでメッセージを取得する関数を定義しています。 export async function getMessages(locale: string) { return (await import(`@/i18n/${locale}.json`)).default; } I18nProviderコンポーネントを使用して、各ページでメッセージを提供します。 import { ReactNode } from 'react'; import { NextIntlClientProvider } from 'next-intl'; export default function I18nProvider({ children, locale, messages }: { children: ReactNode; locale: string; messages: Record<string, Record<string, string>>; }) { return ( <NextIntlClientProvider locale={locale} messages={messages}> {children} </NextIntlClientProvider> ); } SSGの設定 generateStaticParams関数を使用して、静的ページを生成する際のパラメータを設定します。メイン言語(日本語)にはプレフィックスを付けず、その他の言語にはプレフィックスを付けます。 // src/app/[locale]/about/page.tsx export function generateStaticParams() { return locales.filter(locale => locale !== 'ja').map(locale => ({ locale })); } GitHub Pagesへのデプロイ GitHub Actionsを使用して、GitHub Pagesにデプロイします。.github/workflows/deploy.ymlでデプロイの設定を行います。 name: Deploy to GitHub Pages on: push: branches: [main] workflow_dispatch: permissions: contents: read pages: write id-token: write concurrency: group: "pages" cancel-in-progress: false jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: "20" cache: "npm" - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: ./out deploy: environment: github-pages runs-on: ubuntu-latest needs: build steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 リンク GitHubリポジトリ: next-intl-ssg GitHub Pages: next-intl-ssg まとめ このブログでは、Next.jsを使用して多言語対応の静的サイトを構築し、メイン言語にはプレフィックスを付けず、その他の言語にはプレフィックスを付ける方法を紹介しました。next-intlを活用することで、簡単に多言語対応が可能になります。ぜひ試してみてください。 ...

2025年2月28日 · 1 分 · Nakamura

大きな画像から部分画像の座標を取得する方法

概要 大きな画像の一部が切り出された複数の画像から、元の画像内での座標を取得する機会がありました。本記事では、そのための方法についての備忘録をまとめます。 OpenCV の SIFT (Scale-Invariant Feature Transform) を用いて、テンプレート画像と元の画像を特徴点マッチングし、アフィン変換を推定して座標を取得する方法を紹介します。 実装 必要なライブラリ pip install opencv-python numpy tqdm Pythonコード 以下のコードでは、指定した大きな画像 (image_path) に対して、テンプレート画像 (templates_dir 内の PNG 画像) を SIFT でマッチングし、元の画像内の座標を取得します。 import cv2 import numpy as np from glob import glob from tqdm import tqdm import os # 画像読み込み def load_image_gray(path): img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: print(f"画像が見つかりません: {path}") return img # 特徴点抽出 def extract_features(image, detector): return detector.detectAndCompute(image, None) # マッチング処理 def match_features(des1, des2, matcher, ratio_test=0.7, min_matches=4): matches = matcher.knnMatch(des1, des2, k=2) good_matches = [m for m, n in matches if m.distance < ratio_test * n.distance] return good_matches if len(good_matches) >= min_matches else None # アフィン変換推定 def estimate_affine_transform(kp1, kp2, good_matches): src_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2) dst_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2) M_affine, _ = cv2.estimateAffinePartial2D(src_pts, dst_pts, method=cv2.RANSAC, ransacReprojThreshold=5.0) return M_affine # 画像上にマッチング結果を描画 def draw_matched_rectangle(image, M_affine, templ_shape): h, w = templ_shape rect_pts = np.float32([[0, 0], [w, 0], [w, h], [0, h]]) # 長方形の四隅 transformed_pts = cv2.transform(np.array([rect_pts]), M_affine)[0] # 変換後の座標 cv2.polylines(image, [np.int32(transformed_pts)], isClosed=True, color=(0, 0, 255), thickness=2) return transformed_pts # メイン処理 def main(image_path, templates_dir, output_path): # 画像とテンプレート一覧の読み込み img = load_image_gray(image_path) templ_paths = glob(templates_dir) dst_img = cv2.imread(image_path) # SIFT特徴量検出器 & BFMatcher 設定 sift = cv2.SIFT_create() bf = cv2.BFMatcher(cv2.NORM_L2, crossCheck=False) kp1, des1 = extract_features(img, sift) # 特徴点が見つからなかった場合 if des1 is None: print("対象画像の特徴点が見つかりませんでした。") return for templ_path in tqdm(templ_paths): templ = load_image_gray(templ_path) if templ is None: continue kp2, des2 = extract_features(templ, sift) if des2 is None: continue good_matches = match_features(des1, des2, bf) if good_matches is None: print(f"特徴点のマッチングが不足: {templ_path}") continue # アフィン変換推定 M_affine = estimate_affine_transform(kp1, kp2, good_matches) if M_affine is None: print(f"アフィン変換推定に失敗: {templ_path}") continue # 矩形描画 best_dst = draw_matched_rectangle(dst_img, M_affine, templ.shape) # ファイル名を矩形の近くに表示 x, y, _, _ = cv2.boundingRect(best_dst) base_name = os.path.splitext(os.path.basename(templ_path))[0] cv2.putText(dst_img, base_name, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) # 結果を保存 cv2.imwrite(output_path, dst_img) print(f"結果画像を保存しました: {output_path}") 実行 # 実行 if __name__ == "__main__": # パラメータ設定 IMAGE_PATH = "/xxx/default.jpg" TEMPLATES_DIR = "/xxx/*.png" OUTPUT_PATH = "/xxx/match_result.jpg" main(IMAGE_PATH, TEMPLATES_DIR, OUTPUT_PATH) まとめ 本記事では、SIFT を用いた特徴点マッチング によって、部分画像が元画像のどこに位置するかを推定し、アフィン変換 で位置を特定する方法を紹介しました。 ...

2025年2月23日 · 2 分 · Nakamura

Mirador3のFirebase連携annotationsプラグインにおいて、メールアドレスによる登録を可能にしました。

概要 Mirador3のFirebase連携annotationsプラグインを開発しています。 こちらについて、これまではGoogleアカウントによるログイン機能のみを提供していましたが、メールアドレスによるログイン機能を追加しました。 機能紹介 以下、ログインボタンを押した場合です。 メールアドレスによるログインの場合、アカウントの新規作成が可能です。 ログイン後、ユーザに関する情報を表示するようにしました。 アイコンをクリックすると、ログアウトボタンが表示されます。 まとめ IIIFを用いたアノテーションの作成と共有において、参考になりましたら幸いです。

2025年2月21日 · 1 分 · Nakamura

vttファイルからTEI/XMLを作成する

概要 vttファイルからTEI/XMLファイルを作成する方法の備忘録です。 さらに、IIIFマニフェストから、vttファイルおよびTEI/XMLファイルにアクセスできるようにしてみます。結果、以下のように、TEI/XMLファイルがSeeAlsoに関連づけられ、また「Annotations」タブから、vttファイルの内容にアクセスできます。 https://clover-iiif-demo.vercel.app/?manifest=https://movie-tei-demo.vercel.app/data/sdcommons_npl-02FT0102974177/sdcommons_npl-02FT0102974177_vtt.json 参考 以下の「The Ethiopian Language Archive」における取り組みを参考にしました。特に、TEI/XMLの構造化方法が特に参考になりました。 https://dev.jael.info/documentation/ 例 以下で作成したvttファイルを対象とします。 具体的には、以下の『県政ニュース 第1巻』(県立長野図書館)を使用します。 https://www.ro-da.jp/shinshu-dcommons/library/02FT0102974177 TEI/XMLの作成 作成したTEI/XMLファイルの例は以下です。 https://movie-tei-demo.vercel.app/data/sdcommons_npl-02FT0102974177/sdcommons_npl-02FT0102974177.xml 具体的には以下です。 <?xml-model href="http://www.tei-c.org/release/xml/tei/custom/schema/relaxng/tei_all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?> <?xml-model href="http://www.tei-c.org/release/xml/tei/custom/schema/relaxng/tei_all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?> <teiCorpus xmlns="http://www.tei-c.org/ns/1.0"> <teiHeader> <fileDesc> <titleStmt> <title>県政ニュース 第1巻</title> </titleStmt> <publicationStmt> <distributor>中村覚</distributor> <availability> <licence target="http://creativecommons.org/licenses/by/4.0/">http://creativecommons.org/licenses/by/4.0/</licence> </availability> </publicationStmt> <notesStmt> <note>昭和30年に長野県が制作した記録映像。次の8タイトルを収録する。「地方選挙終る」、「地方選挙後初の県議会開かる」、「三十年度を賄う県のお台所」、「すすむ土木建設」、「明るく正しく健やかに」、「幕をとじた善光寺の御開帳」、「勇ましい水防訓練」、「お国じまん民謡大会」</note> </notesStmt> <sourceDesc> <biblStruct> <monogr> <title>県政ニュース 第1巻</title> <availability> <licence target="https://creativecommons.org/publicdomain/zero/1.0/">cc0</licence> </availability> <imprint> <publisher>信州デジタルコモンズ 県立長野図書館所蔵資料</publisher> </imprint> </monogr> <ref target="https://www.ro-da.jp/shinshu-dcommons/library/02FT0102974177">信州デジタルコモンズ 県立長野図書館所蔵資料</ref> </biblStruct> </sourceDesc> </fileDesc> </teiHeader> <TEI> <teiHeader> <fileDesc> <titleStmt> <title>県政ニュース 第1巻</title> </titleStmt> <publicationStmt> <p /> </publicationStmt> <notesStmt> <note /> </notesStmt> <sourceDesc> <p /> </sourceDesc> </fileDesc> <revisionDesc> <change when="2025-02-18"> 作成 </change> </revisionDesc> </teiHeader> <text> <body> <timeline unit="ms"> <when absolute="00:00:00.000" xml:id="t1" /> <when absolute="00:00:25.500" xml:id="t2" /> <when absolute="00:00:38.500" xml:id="t3" /> <when absolute="00:00:50.500" xml:id="t4" /> <when absolute="00:00:55.500" xml:id="t5" /> <when absolute="00:01:03.500" xml:id="t6" /> <when absolute="00:01:08.500" xml:id="t7" /> <when absolute="00:01:18.500" xml:id="t8" /> <when absolute="00:01:23.500" xml:id="t9" /> <when absolute="00:01:33.500" xml:id="t10" /> ... </timeline> <annotationBlock xml:id="ab1"> <u start="#t1" end="#t2">♪♪♪</u> </annotationBlock> <annotationBlock xml:id="ab2"> <u start="#t2" end="#t3">今年は選挙の当たり年。2月の総選挙に引き続いて、4月の県市町村と八木早の選挙で、長野県116万有権者の関心は非常な高まりようです。</u> </annotationBlock> <annotationBlock xml:id="ab3"> <u start="#t3" end="#t4">男女青年や婦人層はもちろんのこと、この老人も今年88を迎えたとはいえ、その慎重な投票ぶりが老いの表に一徹さを伺わせています。</u> </annotationBlock> <annotationBlock xml:id="ab4"> <u start="#t4" end="#t5">♪〜</u> </annotationBlock> <annotationBlock xml:id="ab5"> <u start="#t5" end="#t6">かくて県下における投票率、全国の上位を占める立派な成績を収めました。</u> </annotationBlock> <annotationBlock xml:id="ab6"> <u start="#t6" end="#t7">♪ ♪</u> </annotationBlock> <annotationBlock xml:id="ab7"> <u start="#t7" end="#t8">その日午後8時 きっかり、県下一斉に即日開票が行われました。</u> </annotationBlock> <annotationBlock xml:id="ab8"> <u start="#t8" end="#t9">その結果、長野県知事には、前知事の林寅氏が当選。</u> </annotationBlock> <annotationBlock xml:id="ab9"> <u start="#t9" end="#t10">またこれと同時に、県議会議員61名の当選も決定しました。</u> </annotationBlock> ... </body> </text> </TEI> </teiCorpus> IIIFマニフェストファイルの作成 上述したTEI/XMLファイルをseeAlsoに持つIIIFマニフェストファイルを作成しました。 ...

2025年2月21日 · 3 分 · Nakamura

clover-iiifをNext.jsで使用する

概要 clover-iiifをNext.jsで使用するサンプルリポジトリを作成したので、備忘録です。 https://clover-iiif-demo.vercel.app/ 背景 clover-iiifは以下のように説明されています。 https://github.com/samvera-labs/clover-iiif Extensible IIIF front-end toolkit and Manifest viewer. Accessible. Composable. Open Source. (日本語訳)拡張可能な IIIF フロントエンドツールキットとマニフェストビューア。 これをNext.jsで使用します。 データ 「校異源氏物語(国立国会図書館所蔵)」をサンプルデータとして使用します。 https://dl.ndl.go.jp/pid/3437686 リポジトリ 以下で公開しています。 https://github.com/nakamura196/clover-iiif-demo 以下を参考にしました。 https://samvera-labs.github.io/clover-iiif/docs/composing クライアントサイドでの実行にあたり、以下のような工夫が必要でした。 "use client"; import React, { Suspense } from "react"; import dynamic from "next/dynamic"; import { useSearchParams } from "next/navigation"; // Viewerコンポーネントを動的にインポート(SSRを無効化) const Viewer = dynamic( () => import("@samvera/clover-iiif/viewer"), { ssr: false } ); const WorkContent = () => { const searchParams = useSearchParams(); const manifestId = searchParams.get('manifest') || "https://dl.ndl.go.jp/api/iiif/3437686/manifest.json"; return ( <article> <Viewer iiifContent={manifestId} /> </article> ); }; const Work = () => { return ( <Suspense fallback={<div>Loading...</div>}> <WorkContent /> </Suspense> ); }; export default Work; まとめ 不完全な点もあるかと思いますが、参考になりましたら幸いです。

2025年2月17日 · 1 分 · Nakamura

Mirador 3の mirador-annotations プラグインで、付与したアノテーションをダウンロードする

概要 Mirador 3の mirador-annotations プラグインで、付与したアノテーションをダウンロードするための設定に関する備忘録です。 https://mirador-annotations.vercel.app/ 背景 以下の記事で、アノテーションをGoogleのFirestoreに登録する方法を紹介しました。 ここで登録したアノテーションをダウンロードするにあたり、mirador-annotationsプラグインでダウンロードオプションが提供されていたので、その方法について紹介します。 方法 以下がデモページのソースコードになりますが、exportLocalStorageAnnotationsというオプションをtrueにすることで、ダウンロードアイコンが表示されました。 import mirador from 'mirador/dist/es/src/index'; import annotationPlugins from '../../src'; import LocalStorageAdapter from '../../src/LocalStorageAdapter'; import AnnototAdapter from '../../src/AnnototAdapter'; const endpointUrl = 'http://127.0.0.1:3000/annotations'; const config = { annotation: { adapter: (canvasId) => new LocalStorageAdapter(`localStorage://?canvasId=${canvasId}`), // adapter: (canvasId) => new AnnototAdapter(canvasId, endpointUrl), exportLocalStorageAnnotations: false, // display annotation JSON export button }, id: 'demo', window: { defaultSideBarPanel: 'annotations', sideBarOpenByDefault: true, }, windows: [{ loadedManifest: 'https://iiif.harvardartmuseums.org/manifests/object/299843', }], }; mirador.viewer(config, [...annotationPlugins]); ダウンロードによって得られるJSONファイルの例は以下です。canvas毎にダウンロードできます。 { "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1/annotations", "items": [ { "body": { "type": "TextualBody", "value": "<p>Cを変更</p>" }, "type": "Annotation", "motivation": "commenting", "target": { "selector": [ { "type": "FragmentSelector", "value": "xywh=5314,1983,195,206" }, { "type": "SvgSelector", "value": "<svg xmlns='http://www.w3.org/2000/svg'><path xmlns=\"http://www.w3.org/2000/svg\" d=\"M5314.21383,2086.96256c0,-56.89385 43.74418,-103.01543 97.70536,-103.01543c53.96118,0 97.70536,46.12158 97.70536,103.01543c0,56.89385 -43.74418,103.01543 -97.70536,103.01543c-53.96118,0 -97.70536,-46.12158 -97.70536,-103.01543z\" data-paper-data=\"{"state":null}\" fill=\"none\" fill-rule=\"nonzero\" stroke=\"#00bfff\" stroke-width=\"3\" stroke-linecap=\"butt\" stroke-linejoin=\"miter\" stroke-miterlimit=\"10\" stroke-dasharray=\"\" stroke-dashoffset=\"0\" font-family=\"none\" font-weight=\"none\" font-size=\"none\" text-anchor=\"none\" style=\"mix-blend-mode: normal\"/></svg>" } ], "source": { "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1", "type": "Canvas", "partOf": { "id": "https://dl.ndl.go.jp/api/iiif/3437686/manifest.json", "type": "Manifest" } } }, "canvasId": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1", "created": { "seconds": 1739524276, "nanoseconds": 522000000 }, "id": "exlXeaswdsiuhcFiLWSl", "userName": "中村覚", "manifestId": "https://dl.ndl.go.jp/api/iiif/3437686/manifest.json", "modified": { "seconds": 1739526046, "nanoseconds": 354000000 } }, { "motivation": "commenting", "id": "rx2JMkwtwuQDgbtAIOoO", "created": { "seconds": 1739524259, "nanoseconds": 611000000 }, "manifestId": "https://dl.ndl.go.jp/api/iiif/3437686/manifest.json", "body": { "type": "TextualBody", "value": "<p>校異源氏物語</p>" }, "userName": "中村覚", "canvasId": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1", "target": { "selector": [ { "type": "FragmentSelector", "value": "xywh=478,307,748,3424" }, { "value": "<svg xmlns='http://www.w3.org/2000/svg'><path xmlns=\"http://www.w3.org/2000/svg\" d=\"M478.38073,3732.45418v-3424.85175h748.39353v3424.85175z\" data-paper-data=\"{"state":null}\" fill=\"none\" fill-rule=\"nonzero\" stroke=\"#00bfff\" stroke-width=\"3\" stroke-linecap=\"butt\" stroke-linejoin=\"miter\" stroke-miterlimit=\"10\" stroke-dasharray=\"\" stroke-dashoffset=\"0\" font-family=\"none\" font-weight=\"none\" font-size=\"none\" text-anchor=\"none\" style=\"mix-blend-mode: normal\"/></svg>", "type": "SvgSelector" } ], "source": { "partOf": { "id": "https://dl.ndl.go.jp/api/iiif/3437686/manifest.json", "type": "Manifest" }, "type": "Canvas", "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1" } }, "modified": { "seconds": 1739524259, "nanoseconds": 611000000 }, "type": "Annotation" } ], "type": "AnnotationPage" } まとめ Mirador 3の mirador-annotations プラグインの利用にあたり、参考になりましたら幸いです。 ...

2025年2月14日 · 2 分 · Nakamura

Mirador 3の mirador-annotations プラグイン向けに、Firestore用のアダプタを開発しました。

概要 Mirador 3の mirador-annotations プラグイン向けに、Firestore用のアダプタを開発したので備忘録です。以下でお試しいただけます。 https://mirador-annotations.vercel.app/ 背景 Mirador 3の mirador-annotations プラグインは、デフォルトではローカルストレージにアノテーションが保存されます。 一方、以下の記事で紹介したように、アダプタを変更することで、ローカルストレージではない場所にアノテーションを保存することもできます。 そこで、今回はGoogleのFirestoreに保存するためのアダプタを開発しました。 使い方 ユーザごとにアノテーションを保存できる仕組みとしました。 そのため、まず以下のボタンから、ログインを行います。 ログイン後、自分が付与したアノテーションが表示されます。 アノテーションの登録方法に違いはありません。 ログアウトすると、アノテーションが非表示となります。 リポジトリ ソースコードは以下で公開しています。 https://github.com/nakamura196/mirador-annotations 特に、以下が今回開発したアダプタです。 https://github.com/nakamura196/mirador-annotations/blob/master/src/FirestoreAnnotationAdapter.js また、以下がログインを行うためのボタンです。 https://github.com/nakamura196/mirador-annotations/blob/master/src/GoogleAuthButton.js 開発される際には、.env.exampleを参考に、FirebaseのAPIキーなどを.envに記入します。 https://github.com/nakamura196/mirador-annotations/blob/master/.env.example まとめ 本アプリを使用することで、ユーザごとに、IIIF画像に対するアノテーションを簡単に管理できるようになると思います。 なお、冒頭でご紹介した本アプリのデモ環境に登録されたデータは任意のタイミングで削除する可能性があるのでご注意ください。

2025年2月14日 · 1 分 · Nakamura

Algoliaでページネーションの上限を変更する

概要 Algolia では、検索結果のページネーションに制限 (paginationLimitedTo) が設定されており、デフォルトでは 1,000件 までの検索結果にしかアクセスできません。この設定を変更することで、ページネーションの最大件数を調整できます。 設定方法 ページネーションの上限 (paginationLimitedTo) を変更するには、以下の方法があります。 Algolia ダッシュボードから設定 Algolia にログイン 対象のインデックス を選択 Configuration(設定) → Pagination(ページネーション) を開く paginationLimitedTo の値を変更 その他 API による設定変更も可能なようです。 まとめ この設定を適切に活用することで、より柔軟な検索体験を提供できます。 参考になりましたら幸いです。

2025年2月10日 · 1 分 · Nakamura