ブラウザだけでHEICをPNGに変換する方法(heic-to × WebAssembly)
概要
今回は、ブラウザだけでHEICをPNGに変換する方法について、自作ツールに実際に組み込んだ実装を交えて紹介していきます。
きっかけは、自分で作っているブラウザ完結型の背景透過ツール「とうかねっこ」で、ある困りごとに突き当たったことでした。
iPhoneで撮った写真を読み込ませようとすると、new Image() のところで失敗して、画像がまったく開けないのです。
原因はファイル形式。
iPhoneは標準でHEIC(HEIF)という形式で写真を保存していて、これがChromeやFirefoxではそのまま開けません。
かといって、このツールは画像を一切サーバーに送らないクライアント完結が売りなので、サーバー側で変換する選択肢も取れませんでした。
そこで採用したのが、heic-to というライブラリをWebAssembly(WASM)で動かして、ブラウザの中だけでHEICをPNGに変換するという方法です。
この記事では、なぜブラウザでHEICが開けないのか、ライブラリに何を選びどう組み込んだのか、そして実地でハマった「最後の1割」まで、判断の理由ごと書いていきます。
それではやっていきましょう!
バージョンは記事執筆時点でheic-to v1.5.2 / libheif 1.22.xです。
目次
なぜブラウザはHEICをそのまま開けないのか
まず大前提として、HEICをそのまま <img> で表示できるブラウザはSafari 17以降だけです(macOS Sonoma / iOS 17以降)。
Chrome・Firefox・Edgeは、執筆時点でもHEICのネイティブデコードに対応していません。
理由はざっくり言うと、HEICの中身がHEVCというコーデックで圧縮されていて、そのライセンス事情などから主要ブラウザがデコーダを積んでいないためです。
Safariだけ開けるのは、OS側のHEICサポートに乗っかっているからですね。
なので、new Image() + URL.createObjectURL(blob) で読み込んでCanvasに描く、という普通の読み込み線がHEICだけ通らない。
これがこの問題の核心でした(- -;
このツールの制約(設計の前提)
解決策を考える前に、このツールが背負っている設計の前提を整理しておきます。
ここがそのまま「取れる手段」を縛るからです。
- ノービルド(vanilla JS)
- バンドラを使わず素のJavaScriptで書く方針。重いライブラリを常時読み込ませたくない
- 画像はサーバーに送らない
- プライバシー重視で、変換も含めてすべて端末内で完結させる。これは絶対に崩せない根幹
- Cloudflare Pagesで静的配信
- サーバーサイドの処理を持たない。つまり「サーバーでHEICを変換」は最初から選択肢に無い
サーバー変換が使えない以上、ブラウザの中だけでHEICをデコードしきるしかありません。
そして、ブラウザ内でHEICを開く現実解は、結局1つに収束します。
解決策:libheifをWASMで動かす
ブラウザ内でHEICをデコードする、と言ったときの実体は、ほぼ例外なくC言語の libheif をEmscriptenでWebAssembly化したものです。heic2any / heic-to / libheif-js など主要ライブラリはどれも、中身はlibheif。
つまり選択肢は「libheifを直接使うか、薄いラッパー経由で使うか」の違いに過ぎず、どれを選んでもLGPLは共通でついてきます(ここは後述します)。
ラッパーを使う意味は、isHeic() のようなヘルパーや、wasmの同梱・読み込みまわりの面倒を肩代わりしてくれるところにあります。
自分でwasmをビルド・保守したくないなら、メンテされているラッパーに乗るのが現実的です。
heic-toを選んだ理由
候補は heic2any と heic-to の2つでした。
機能としてはどちらもHEIC→PNG/JPEGができて同等です。
ただ、決め手はメンテナンスが継続しているかでした。
| 観点 | heic2any | heic-to(採用) |
|---|---|---|
| 中身 | libheif(LGPL) | libheif(LGPL) |
| メンテ状況 | 12ヶ月以上リリース停止 | 積極的にメンテ中 |
| libheifの追従 | 古いまま | 新版に追従(1.22.x) |
| 配布形態 | — | 単一IIFE・wasm内包 |
| 実務ヘルパ | — | isHeic() 等あり / CSP対応ビルドも |
heic2any はラッパー部分こそMITですが、リリースが1年以上止まっていて、同梱されるlibheifが古いままになります。
画像パーサは過去にCVE実績がある領域なので、上流のセキュリティ修正を取り込めないのは地味に怖いですよね。
その点 heic-to は積極的にメンテされていて、libheifの新版に追従しています。
さらに単一のIIFEファイルにwasmまで内包しているので、別の .wasm ファイルをホストする必要がなく、オフラインでも動きます。
ポイントは、これは「libheifを避けた」のではなく、libheifを最小コストで安全に使い続けるための選択だということです。
「自分でwasmをビルド・保守したくない、でも上流のセキュリティ修正は受け取りたい」——このわがままを両立してくれるのがheic-toでした。
約3MBを「必要なときだけ」読む(遅延読込)
heic-toは約3MB(wasmをJSに内包しているぶん大きい)あります。
これを全ユーザーに常時ロードさせるのは、さすがに過剰です。
だって、HEICを使わずJPG/PNGを読み込むだけの人が大半ですから。
そこでHTMLには重いライブラリ本体を書かず、HEICが選ばれた瞬間にだけ <script> を動的注入する軽量ローダ js/heic.js を用意しました。
処理の流れはこんなイメージです。
肝は ensureLoaded() で、一度だけ注入してPromiseをメモ化します。
何度HEICを読み込んでも、ライブラリの取得は最初の1回きりです。
js/heic.js
1 | var LIB_SRC = "js/vendor/heic-to.js"; // IIFE 版。グローバル HeicTo を公開 |
onerror で loadPromise を null に戻しているのは、読み込みに失敗したとき次回リトライできるようにするためです。
効果としては、通常のJPG/PNG利用では js/vendor/heic-to.js へのリクエストが一切発生しません。
「HEICを選ぶまでロード0」はE2Eでも検証しています。
実装:検出 → 変換 → 既存フローへ合流
組み込みは3ステップです。
HEICかどうかを判定し、HEICならPNGに変換し、あとは既存の読み込み線にそのまま合流させる。
既存コードへの傷を最小化したかったので、HEICのときだけ前段に変換を1枚挟む形にしました。
HEIC検出は「MIME + 拡張子」の二段
最初につまずきやすいのがここです。File.type(MIME)だけ見て判定すると、HEICを取りこぼすことがあります。
js/heic.js
1 | /** ファイルが HEIC/HEIF か(type は空のことがあるため拡張子も見る)。 */ |
この二段判定にしておくと、「ファイル選択」「ドラッグ&ドロップ」のどちらの入口からでも、安定してHEICを拾えます。
変換はタイムアウト付きでPNGへ
判定でHEICだとわかったら、PNGに変換します。
ここで重要なのがタイムアウトを必ず入れることです。
端末が遅かったり、特殊なHEICだったりで万一ハングしても、無限待ちにならないようにします。
js/heic.js
1 | var CONVERT_TIMEOUT_MS = 90000; // 端末が遅い/巨大画像でも余裕を持たせつつ、無限待ちは避ける |
Promise.race で「変換」と「90秒タイマー」を競わせて、早いほうを採用します。
失敗時には console.error を残しておくと、特定のHEICで失敗したときに原因を追いやすくなります。
変換後はこんなふうに編集画面へ画像が載ります。
ちなみに実測では、4032×3024のHEICが約3秒でPNG化できました(PNGで約2.2MB)。
ただしこれはデスクトップのChromiumで測った一例で、端末・画像サイズ・環境でかなり変わります。
スマホの実機ではもっと遅くなることもあるので、数字は目安として捉えてください。
入口2か所を共通ヘルパに寄せる
このツールには画像の入口が2つあります。
画像選択画面(start)と、編集画面でのドラッグ&ドロップ差し替え(app)です。
どちらも「HEICなら変換してから既存処理へ」という同じ共通ヘルパ HeicSupport.toPngBlob() に寄せました。
js/start.js
1 | function handleFile(file) { |
通常画像は Promise.resolve(file) でそのまま素通り、HEICのときだけ toPngBlob() を挟む。
start側は変換後のPNG BlobをIndexedDB経由で編集画面へ受け渡し、app側は変換後のPNGを new Image() で読み込みます。
既存の受け渡し機構・読み込み線をそのまま流用できるので、HEIC対応のための分岐は本当に「前段に1枚」だけで済みました。
なぜPNGに変換するのか
変換先をPNGにしたのには、はっきりした理由があります。
このツールの本質は背景透過=縁のピクセル精度が品質を左右するところにあります。
ここでJPEGに変換してしまうと、再圧縮で縁がにじんで劣化しうる。
なので、可逆なPNGに変換してから編集に渡しています。
LGPL-3.0とどう向き合ったか
ここはこの記事の見せ場のひとつです。
HEIC対応にlibheifは不可避なので、LGPL-3.0は必ずついてきます。
これは「悪いこと」ではなく、満たすべき要件として淡々と向き合えばいいだけです。
LGPLの “Lesser” たる所以は、条件を守れば自前コードにライセンスを伝播させずに使えるところにあります。
具体的にはこうしました。
- ライブラリを改変しない
- 配布されたheic-toをそのまま使う。手を入れると話がややこしくなる
- 独立した単一ファイルとして同梱
js/vendor/heic-to.jsとして切り離しておき、自分のアプリコードと混ぜない
- 動的読込で利用する
- 利用者がライブラリを差し替えられる状態を保つ。これがLGPLの肝
これで、自分のアプリコードは従来どおり非公開のままでも合法に使えます。
そのうえで、頒布側の義務として次のことをやりました。
- ライセンス全文を無改変で同梱・配信
/js/vendor/heic-to.LICENSE.txtを200で配信する
- エンドユーザーが見られる告知を用意
- 免責ページの「オープンソースソフトウェア」節に、出典・LGPL・全文リンクを明示
- 出典(GitHub)リンクを明示
- heic-to / libheifのリポジトリへのリンクを置く
見落としがちなのは、heic-toは訪問者のブラウザにダウンロードされる=「頒布」にあたるという点です。
リポジトリ内のREADMEに書いておくだけでは不足で、配信ページ上でエンドユーザーに告知する必要があります。
「LGPLライブラリをWebで配る=ユーザーにバイナリを頒布している」という意識が、ここでの要点でした。
実地で出た「最後の1割」:トーストが消えるバグ
最後に、実装の正しさとUXの正しさは別物だ、と痛感した失敗談を1つ。
ドラッグ&ドロップでHEICを入れると、「HEICを変換中…」と出たあと、何も起きないように見える症状が出ました。
調べてみると、ドロップ処理のコード自体は正しくて、合成ドロップ+実機サイズ(4032×3024)でも約3秒できちんと成功していました。
犯人は表示のほうだったのです。
「変換中」トーストが1.9秒で自動的に消える実装になっていて、実機HEICの変換が数秒かかる間にトーストが先に消えてしまう。
だから「無反応」に見えていた、というオチでした(- -;
対処は4つです。
- 変換中トーストをstickyに
- 完了/失敗まで消さない。
sticky=trueの間は自動で消さないようにした
- 完了/失敗まで消さない。
- 90秒タイムアウトを追加
- 万一ハングしても、必ずエラー表示に落ちるようにする
- 変換後の
new Image()にonerrorを追加- 読み込み失敗も無反応にせず、ちゃんと拾う
- 失敗時に
console.errorを出す- 特定のHEICで失敗したとき、原因を追えるようにする
stickyトーストの切り替えはこんな実装です。
js/start.js
1 | var toastTimer; |
編集画面側でも、変換後の new Image() に onerror を足して、読み込み失敗を取りこぼさないようにしています。
js/app.js
1 | prepared.then((blob) => { |
教訓は、非同期な重い処理は「進捗が見えること」まで含めて初めて完成するということ。
コードが正しく動いていても、ユーザーに伝わらなければ「壊れている」のと同じなんですよね。
まとめ
ブラウザだけでHEICをPNGに変換する、という今回の対応を振り返ると、勘所は次のあたりに集約されます。
- 現実解はlibheifをWASMで動かすこと
- heic-toを選んだのは、メンテ継続=上流のセキュリティ修正に追従できるから
- 重いライブラリは必要なときだけ読む
- 約3MBを遅延読込にして、通常利用には一切負担をかけない
- HEICのときだけ前段に変換を1枚挟む
- 検出は「MIME + 拡張子」の二段、変換はタイムアウト付き、あとは既存フローへ合流
- LGPLとUXは“最後の1割”
- Webで配る=頒布の意識、そして進捗が見えるまでが実装の完成
クライアント完結のまま、HEIC対応は十分に実用的です。
画像をサーバーに送らないという根幹を守ったまま、iPhoneの写真もちゃんと扱えるようになりました。
同じようにWebでHEICを扱いたい場面は意外と多いと思うので、ぜひ参考にしてみてください。
それではやっていきましょう(^^
採用したライブラリ(heic-to)
github.comhttps://github.com/hoppergee/heic-to
HEICデコードの本体(libheif・LGPL)
github.comhttps://github.com/strukturag/libheif