PicFlip ができるまで — ブラウザ内 libheif + Canvas で画像形式変換を成立させる設計
heic2any (libheif WASM) と Canvas API で HEIC / JPG / PNG / WebP のクロス変換をブラウザ完結で実装したWebサービスの設計記録。バッチ・品質スライダー・暗室セーフライト演出までを解説します。
PicFlip ができるまで — ブラウザ内 libheif + Canvas で画像形式変換を成立させる設計
PicFlip は、画像形式 (HEIC / JPG / PNG / WebP) を アップロードせずに 変換するWebサービスです。 voice-scribe / clip-cast / bg-snap / text-pluck / pdf-anvil / pixel-lift に続く、メディア処理ラボの 7本目。 「iPhone の HEIC を Windows / Android で開ける形式にしたい」 という universal な需要をブラウザ完結で解く。
なぜこの形にしたか
「HEIC 変換」 「PNG JPG 変換」 は日本語圏でも巨大な検索需要で、 既存サービスはほぼ全部アップロード前提です。 個人写真や家族の写真をアップロードしない代替を出せれば、 説明不要の差別化になります。
heic2any は libheif (Apple HEIC 仕様のオープン実装) を WebAssembly でブラウザに持ち込み、 数行で HEIC を Blob にデコードできる。 ここから先は Canvas で JPG / PNG / WebP のクロス変換ができる。 全部ブラウザの中だけで完結します。
visual direction
暗室セーフライト × ローズレッド。
- ほぼ黒 (
#0a0608) を背景に、 ローズレッド (#f43f5e) を主アクセント、 ピンク (#fda4af) をハイライトに - 「フィルム現像の安全光」 をモチーフにして、 既存6本 (mint / amber / violet / cobalt / lime / gold) と完全に別の温度帯
- 形式変換 = 「セーフライトの下で別の媒体に焼き付け直す」 という暗室メタファーをUI全体に貫いている
- アクセントを
linear-gradient(135deg, rose → pink)でグラデーション化することで、 単色ではなく色の遷移自体が「形式を切り替える」 動作を表現
実装の見どころ
1. HEIC → JPG/PNG/WebP の分岐
heic2any は toType: image/jpeg | image/png のみネイティブ対応で、 WebP は出力できない。 そこで:
const intermediate = toFormat === "webp" ? "image/jpeg" : MIME_FOR_FORMAT[toFormat]
const out = await heic2any({ blob: file, toType: intermediate, quality })
if (toFormat !== "webp") return out
return await reencodeViaCanvas(out, "webp", quality)
HEIC → JPEG (libheif) → WebP (Canvas) の2段経路で WebP もカバーします。
2. JPG ↔ PNG ↔ WebP のクロス変換 (Canvas)
非HEICの入力は createImageBitmap で ImageBitmap にデコードし、 Canvas 経由で目的形式に再エンコード。 JPEG 出力時だけ白背景を敷いてアルファチャネル落ちを目立たせない処理を入れています。
if (toFormat === "jpeg") {
ctx.fillStyle = "#ffffff"
ctx.fillRect(0, 0, canvas.width, canvas.height)
}
ctx.drawImage(bitmap, 0, 0)
canvas.toBlob(resolve, MIME_FOR_FORMAT[toFormat], quality)
3. 一括バッチ処理
convertBatch(files, format, quality, onProgress) で複数ファイルを順次処理。 失敗したファイルは failures 配列に蓄積され、 成功分とは別にエラーリストとして UI に表示します。 「10ファイル投げて9枚成功した」 という現実的なケースで、 成功分を捨てずに保存できるようにしました。
4. quality スライダー (lossy 形式のみ)
JPG / WebP は quality 0.1-1.0 で書き出しサイズが大きく変わります。 PNG は常に lossless なので、 出力形式を PNG に切り替えると quality スライダーは自動で隠れます。 「設定が見えていれば触れる、効かない設定は見せない」 原則です。
5. EXIF の扱い
Canvas を介する変換だと EXIF はほぼ落ちます。 これは多くのユーザーにとってむしろ望ましい挙動 (位置情報や撮影機種の漏洩を防げる) ですが、 「撮影日を残したい」 ケースもあるので、 EXIF 保持 / 完全 strip のトグルを今後の拡張として FAQ に予告しています。
苦労したところ
- heic2any のメモリリーク疑惑: 大きい HEIC を連続変換すると WASM ヒープが伸びる傾向があった。 ファイル単位で再 import して回避する手も検討。 v1 では実用範囲なので保留。
- iOS Safari の
image/heicMIME 認識: ファイルピッカーでimage/*を指定しても HEIC が選択肢に出ないケースがあるため、 input のacceptに.heic,.heifを明示。 - WebP 経路の品質ロス: HEIC → JPEG → WebP の2段経路は、 JPEGデコード時にロスが出る。 高品質を要求するなら HEIC→PNG→WebP の lossless 中間案も将来選択肢に。
今後の拡張
- AVIF / TIFF 対応 (libavif / libtiff WASM)
- RAW 対応 (libraw WASM、 ただし依存が重い)
- EXIF 保持 / 完全 strip トグル + EXIF ビューワ
- リサイズ + 形式変換の一気通貫 (PixelLift と統合する余地あり)
- clip-cast との連携: 動画フレームを抽出して PicFlip に流し込む
- bg-snap / PixelLift との連携導線: PicFlip で JPG にしてから背景透過、高解像度化、までを1ページで
このサービスから言える事
メディア処理ラボの 7本柱:
- voice-scribe / clip-cast / bg-snap / text-pluck / pdf-anvil / pixel-lift / pic-flip
「形式変換」 という一見軽い領域も含めて、 メディアまわりの実用ニーズを 7方向でカバーできるようになりました。 ブラウザ完結という制約だけ守れば、 デスクトップアプリ並みの実用品を毎週でも生み出せる、という実感が積み上がっています。
Next Action
読んだあとに、そのまま使って確かめる。
この開発ログは PicFlip をどう育てているかの記録です。読んだらそのままサービス本体へ戻って、価値を確かめられるようにしています。