
WebアプリでのHEIC変換・スマートクロップ・一括アップロードのベストプラクティス
iPhone ユーザーが HEIC をそのままアップロードすると、Chrome / Firefox / Edge では Canvas や <img> が解釈できず、ストレージには保存されているのに画面上では表示されないという事象が起きやすい。加えて HEIC は「1枚の画像」というより複数ストリームを抱えたコンテナなので、古いデコーダでは「同じ HEIC なのに変換できるものとできないものが混在する」という挙動にもつながる。
この記事では、実案件で整理した HEIC 変換を安定させる設計と、スマートクロップ・一括アップロードまわりのベストプラクティスをまとめる。
なぜ「成功と失敗が混在」するのか
HEIC が単一フォーマットではなくコンテナであること、および iOS 側の進化が重なり、主に次の要因でデコードが不安定になりやすい。
| 要因 | 発生しやすい条件 | 典型的な症状 |
|---|---|---|
| HDR ゲインマップ | iOS 18 以降のカメラ、tmap 等の補助画像 |
パースエラーで変換不能 |
| マルチレイヤー / Live Photos | 複数レイヤー・ビデオトラック等 | 主画像が取れず真っ黒・破損 |
| タイル分割の高解像度 | グリッドタイルのパラメータ不整合 | 一部タイルのピクセル破損 |
| 拡張子の大文字 | .HEIC(共有経由など) |
MIME / 拡張子判定で弾かれる |
対策の方向性はシンプルで、最新の libheif 系デコーダ、Primary(主画像)だけを明示的に使う、拡張子・MIME を正規化する、の三つに集約できる。
HEIC 変換のライブラリ選定
長く使われてきた heic2any は内部の libheif が追従しづらく、iOS 18 以降のメタデータに弱いケースが報告されている。フロントで WASM を許容できるなら libheif-js(libheif 1.18+ / 1.19 系) のように、 upstream の修正が入ったビルドを選ぶのが現実的なベストプラクティスである。
- 未知の補助画像があっても落ちにくく、SDR の主画像を優先してデコードできる実装が望ましい。
- ブラウザの HEIC ネイティブ表示は、HEVC ライセンスの事情もあり当面は不透明。WASM で JPEG / WebP に寄せてから扱う設計は、少なくとも数年単位では有効な防衛線になる。
変換パイプラインで押さえるべきポイント
1. マジックバイトによる二重検証
拡張子と File.type だけでは、偽装や不一致に弱い。ファイル先頭で ftyp ボックスとブランド(例: heic, heix, mif1 等)を確認し、HEIC らしさをバイナリレベルで担保する。
2. 拡張子の正規化
判定は toLowerCase() で統一。.HEIC だけ特別扱いするのではなく、すべての分岐で同じルールに揃える。
3. Primary Image のみを使う
decode 結果が複数画像を返す場合、配列の先頭=主画像に寄せる。Live Photos や深度マップは無視し、表示用には SDR の主画像だけを使う。これで「黒い画像」系の事故を大きく減らせる。
Web Worker とフォールバック
libheif-js の WASM はサイズも処理も重い。メインスレッドで直実行すると UI が固まるため、Worker に逃がすのが定石である。
- ArrayBuffer は
transferableで渡すとコピー負荷を抑えられる。 - Worker 内で OffscreenCanvas まで使えれば、デコード〜エンコードを Worker 側で完結できる。
- Safari の対応状況に応じて
OFFSCREEN_CANVAS_NOT_SUPPORTED等でメインスレッドにフォールバックする分岐を用意する。
「常に Worker」ではなく、失敗時の一本道を決めておくと運用が楽になる。
メモリと一括処理の扱い
大きな HEIC はデコード時に一時的に数十〜100MB 級のメモリを食うことがある。数十〜数百枚を扱う場合は、並列より順次(または限られた並列度)にしてメモリピークを抑えるのが安全である。必要なら プリフライトで JPEG は変換スキップなど、無駄な WASM 起動を減らす。
スマートクロップのベストプラクティス
手動クロップだけでは一括運用が破綻する。アスペクト比が固定された UI(サムネイル、カード、ヒーローなど)では、コンテンツを意識した自動クロップを挟むと UX が段違いになる。
smartcrop.js を使う場合の考え方
代表的な流れは次のとおり(アルゴリズムのイメージ)。
- 処理しやすいサイズに縮小
- ラプラス等でエッジを見る
- 彩度マップで「目立つ領域」を強調
- 指定アスペクト比のウィンドウを動かしながらスコアリング
- 三分割法(Rule of Thirds) などの構図ウェイトで最良領域を選ぶ
実装では、目標アスペクト比に合わせた width / height を先に決めてから crop() に渡すと、UI 要件と整合しやすい。
精度はシーン依存が大きい。ポートレート単体では強いが、複数人や意図的な余白の大きい構図では外しやすい。そのため「自動 100%」ではなく、後述のプレビューと手動オーバーライドを前提に設計するのがベストプラクティスである。
クロップ UI
アスペクト比はシステム側でロックし、ユーザーにはパン・ズーム中心に任せると、レイアウト崩れを防げる。react-easy-crop のような軽量なクロッパと組み合わせる例が多い。
一括アップロードのベストプラクティス
次のようなパイプラインが現場で扱いやすい。
- フォルダ / 複数選択でファイル列を受け取る
- 各ファイルを 順次
normalize(HEIC→JPEG 等)→ スマートクロップ座標算出 → プレビュー用 URL 生成 - グリッドでサムネイル一覧。クリックでモーダルから微調整
- 除外 / スキップを個別に許可
- 確定時にだけ 実クロップ + アップロード + DB 更新
ここでのキーメッセージは一つでよい。
システムが大半を自動で行い、例外だけ人が直す(例: 95% / 5% イメージ)
「全部自動で完璧」を目指すより、一覧で異常値に気づける UI を優先する方が結果的に速い。
バックエンド前提のスマートクロップも選択肢
フロントの smartcrop で足りない場合は、顔・被写体検出付きの API(クラウドの Auto Crop、Rekognition、自前の YOLO 等)を検討する。コストとレイテンシ、プライバシー(画像をどこに送るか)とのトレードオフになる。
まとめ
- HEIC はコンテナとして扱い、最新 libheif 系 + Primary のみ + 拡張子正規化 + マジックバイトで安定化させる。
- Worker + OffscreenCanvas + フォールバックで UI を守る。
- 一括処理はメモリを見て順次 / 並列度制限。
- スマートクロップは、自動提案・グリッドでの確認・個別修正の三層が現実的なベストプラクティス。
- 一括アップロードはパイプラインを短いステップに分割し、確定前に一覧で検証できるようにする。
個人開発の電子パンフレット案件でも、同じ方針で iPhone 写真まわりのクレームをかなり減らせた。同様の要件がある方の参考になれば幸いである。