[Astro] #119 JP2 (JPEG 2000) Image Converter — WASM + Canvas + JSZipによるブラウザ完結型画像変換・ZIP一括出力

[Astro] #119 JP2 (JPEG 2000) Image Converter — WASM + Canvas + JSZipによるブラウザ完結型画像変換・ZIP一括出力

概要

一部のアーカイブ資料や高精度画像フォーマットとして利用されるJPEG 2000(.jp2)ファイルを、サーバーにアップロードすることなくブラウザローカル環境のみで標準的な画像フォーマット(PNG / JPEG)に変換・プレビュー表示するWEBツールを構築しました。

デコード処理にはOpenJPEGのWASMビルドを使用し、解像度やアスペクト比を維持したままCanvas上に描画。複数ファイルの一括ドラッグ&ドロップおよびJSZipを使用した一括ZIPエクスポートに対応しています。

スクリーンショット

[Astro] #119 JP2 Image Converter

動画(GIF)

[Astro] #119 JP2 Image Converter

Sample data

sampleは以下のサイトからお借りしています。

今回の実装内容

機能・モジュール概要
JP2デコードOpenJPEG WASM による JPEG 2000 バイナリの高速デコード
Canvasプレビューwindow.devicePixelRatio (DPR) を考慮した高解像度(Retina/4K)クリア描画
メモリ最適化エクスポートcanvas.toBlob()URL.createObjectURL を使った低メモリフットプリントなファイル出力
一括ZIPダウンロードJSZip を統合し、複数変換済み画像を converted_jp2_images.zip としてまとめて圧縮出力
ドラッグ&ドロップUI複数 .jp2 ファイルの一括追加と個別・全体進捗インジケーター

背景 — JPEG 2000のブラウザ非対応問題と解決策

JPEG 2000(.jp2)は高圧縮率かつ可逆・不可逆圧縮をサポートする優秀な画像フォーマットであるものの、モダンブラウザ(Chrome/Firefox/Edgeなど)で標準表示がサポートされていない。そのため、旧アーカイブ画像や地図データなどのJP2ファイルを閲覧・利用するには専用ソフトウェアやオンライン変換サービスに依存する必要がありました。

しかし、セキュリティやプライバシーの観点から外部サーバーに画像をアップロードしたくないケースも多い。本ツールでは、WebAssembly (WASM) を用いてブラウザ内部でOpenJPEGのC言語デコーダーを動作させることで、完全にローカル完結な変換処理を実現しています。


.jp2 ファイル (ドラッグ&ドロップ)
→ OpenJPEG (WASM デコーダー)
→ RGBA ピクセル配列 (ImageData)
→ Canvas API (DPRスケーリング & プレビュー表示)
→ canvas.toBlob() (メモリ最適化)
→ JSZip (一括ZIPバッチダウンロード)

技術実装の詳細

1. WASMデコードとCanvasピクセル復元

.jp2 ファイルを ArrayBuffer として読み込み、OpenJPEG WASM モジュールへ渡して生のRGBAピクセル配列を取り出す。取得したピクセルデータから ImageData を作成し、キャンバスに描画。

// JP2バイナリのデコード
const arrayBuffer = await file.arrayBuffer();
const decompressed = openjpegWasm.decode(new Uint8Array(arrayBuffer));

// ImageDataの生成と描画
const imageData = new ImageData(
  new UintClampedArray(decompressed.data),
  decompressed.width,
  decompressed.height
);

ctx.putImageData(imageData, 0, 0);

2. 高解像度ディスプレイ(DPR)対応

Retinaディスプレイや4Kモニター等の高DPI環境でプレビュー画像がボヤけるのを防ぐため、window.devicePixelRatio を取得してキャンバスの内部解像度とCSS表示サイズを分離制御。

function renderPreview(canvas: HTMLCanvasElement, imgData: ImageData) {
  const dpr = window.devicePixelRatio || 1;
  const ctx = canvas.getContext('2d');
  if (!ctx) return;

  // 論理サイズ(CSS表示サイズ)の設定
  canvas.style.width = `${imgData.width}px`;
  canvas.style.height = `${imgData.height}px`;

  // 物理解像度(DPR考慮)の設定
  canvas.width = imgData.width * dpr;
  canvas.height = imgData.height * dpr;

  // コンテキストのスケール調整
  ctx.scale(dpr, dpr);

  // スケール適用後に一時キャンバス等から描画
  ctx.drawImage(offscreenCanvas, 0, 0);
}

3. toBlob を活用したメモリ最適化

巨大な解像度の画像をエクスポートする際、従来の canvas.toDataURL()(Base64文字列化)を使用すると文字列変換に伴うメモリ空間の急速な圧迫(元のデータ量の約1.33倍+文字列メモリ割り当て)が発生し、ブラウザがクラッシュするリスクがありました。

これを回避するため、非同期 canvas.toBlob() を用いて Blob オブジェクトを直接生成し、URL.createObjectURL 経由でエクスポートを行う設計に変更しました。

async function exportImage(canvas: HTMLCanvasElement, mimeType = 'image/png'): Promise<Blob> {
  return new Promise((resolve, reject) => {
    canvas.toBlob((blob) => {
      if (blob) {
        resolve(blob);
      } else {
        reject(new Error('Canvas to Blob conversion failed'));
      }
    }, mimeType, 0.92);
  });
}

// 単体ダウンロード時の実行例
const blob = await exportImage(canvas, 'image/png');
const downloadUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = `${fileName}.png`;
a.click();
URL.revokeObjectURL(downloadUrl); // メモリ解放

4. JSZip によるバッチ ZIP パッキング

複数の .jp2 ファイルがドロップされた場合、非同期で順次デコード・変換を行い、得られた Blob を JSZip インスタンスに追加。

全ファイルの変換完了後、ZIPファイルをストリーム生成して一括ダウンロード。

import JSZip from 'jszip';

async function handleBatchDownload(convertedFiles: { name: string; blob: Blob }[]) {
  const zip = new JSZip();

  // 各ファイルをZIP構造へ追加
  convertedFiles.forEach(({ name, blob }) => {
    zip.file(name, blob);
  });

  // ZIPアーカイブの生成
  const zipContent = await zip.generateAsync({ type: 'blob' });

  // ダウンロード実行
  const zipUrl = URL.createObjectURL(zipContent);
  const link = document.createElement('a');
  link.href = zipUrl;
  link.download = 'converted_jp2_images.zip';
  link.click();
  URL.revokeObjectURL(zipUrl);
}

技術スタック

  • Astro — ページ・コンポーネント構造
  • OpenJPEG (WASM) — C言語ベースの高速 JPEG 2000 デコーダー
  • Canvas API — ピクセル復元、DPR制御、画像フォーマットエンコード(PNG / JPEG)
  • JSZip — 変換後の複数画像をクライアントサイドでZIP圧縮

更新履歴

  • [v1.0]: 公開バージョン。Retina/DPR 描画対応、toBlob によるメモリ最適化、JSZip 統合による一括 ZIP バッチダウンロード(converted_jp2_images.zip)に対応。
  • [v0.1]: 初期実装。OpenJPEG WASM による .jp2 デコード、単一キャンバス描画および画像出力。