ExifPeel ができるまで — ブラウザだけで画像のEXIFを確認・剥離する設計
exifr (EXIF パーサ) と piexifjs (JPEG EXIF 操作) と Canvas API をブラウザ内で動かし、 GPS / 撮影機種 / 編集ソフト履歴を覗いて剥がすツールの設計記録。 JPEG ロスレス strip と PNG/WebP の Canvas 再エンコード戦略、 GPS 警告 UI まで解説します。
ExifPeel ができるまで — ブラウザだけで画像の中身を覗いて剥がす設計
ExifPeel は、画像のメタデータ (EXIF / GPS / 撮影情報) を サーバーに送らずに 確認・剥離する Webサービスです。 exifr (パース) + piexifjs (JPEG ロスレス strip) + Canvas API (他形式の再エンコード strip) をブラウザ内だけで組み合わせ、 写真は一歩もデバイス外に出ません。 メディア処理ラボ 10本柱に続く、 11本目。 「処理」 でも 「変換」 でも 「転送」 でもなく、 「インスペクション + プライバシー保全」 という新カテゴリで設計しました。
なぜこの形にしたか
iPhone や Android で撮った写真の EXIF には、 多くの場合:
- 撮影場所の 緯度経度 (自宅・職場・行動圏の即バレ)
- 撮影機種 / レンズ / シリアル番号
- 編集ソフトの履歴 (Photoshop / Lightroom / カメラの内蔵ファームウェア)
- ホスト端末名 ("Taro's iPhone" 等)
が埋め込まれています。 SNS にそのまま投稿してプライバシー事故というのは何年も繰り返されている話で、 検査・剥離ツールへの日常的な需要はあります。 ただ既存の Web ツールは画像をいったんサーバーへ POST する作りが多く、 「個人写真の GPS を抜くためにまずサーバーに渡す」 という前提自体に違和感がありました。
exifr と piexifjs は pure JS / WASM 不要で、 ブラウザ完結で組み合わせれば一切アップロードなしのメタデータ検査 + 剥離が成立する。 ラボ thesis にそのまま乗ります。
visual direction
フォレンジック・クロム × ウラニウム・グロウ。
- 深いスレート (
#0c1014) を背景に、 ボーンホワイト (#e2e8f0) のテキスト - ウラニウム・グロウ (lime-200
#d9f99d/ lime-300#bef264) を主アクセントに、 鑑識ルームのワークライトのような蛍光 - GPS 検出時はハザード・マゼンタ (
#ec4899) で警告を強調 - 既存10本 (mint / amber / violet / cobalt / lime / gold / rose / indigo / emerald / vermilion) と完全別軸の cool monochrome 系
「鑑識ルームで写真のレイヤーを 1枚ずつ剥がして調べる」 メタファー。
実装の見どころ
1. exifr で全フィールドを引き出す
exifr.parse(file, { gps, tiff, exif, iptc, xmp, ifd0, ifd1, ihdr, jfif, translateKeys, translateValues, reviveValues, mergeOutput }) を 1発で呼んで、 全 segment のフィールドを 1枚のフラットなオブジェクトとして取り出します。 GPS 経緯度は exifr が自動的に latitude / longitude の数値プロパティに正規化してくれるので、 後段の判定が楽。
const raw = await exifr.parse(file, {
gps: true, tiff: true, ifd0: true, ifd1: true, exif: true,
iptc: true, xmp: true, mergeOutput: true,
})
撮影日時のような DateTimeOriginal は Date オブジェクトとして返ってくるので、 表示時には ISO 化して整形しています。
2. グループ分けして表示
EXIF は 100フィールド近く返ることがあるので、 そのまま全部出すと読めません。 4 つの意味ブロックに分割:
- GPS (緯度経度・高度) — 別パネル + ハザード色 + OpenStreetMap リンク
- カメラ / レンズ (Make / Model / LensModel / シリアル)
- 撮影設定 (DateTimeOriginal / ExposureTime / FNumber / ISO / FocalLength 等)
- ソフトウェア / 端末 (Software / HostComputer)
- 作成者 / 著作権 (Artist / Copyright / UserComment / XPAuthor)
各グループはホワイトリストで拾うので、 「読めるけど意味の解釈に専門知識が要る」 ような bytes-level フィールドは混ぜずに済みます。
3. JPEG はロスレスで EXIF だけ剥がす
JPEG の EXIF は APP1 セグメントに格納されているので、 piexifjs の remove() で マーカーごと抜くだけで完了。 画素データ自体は触らないので 画質劣化ゼロ。
const dataUrl = await fileToDataUrl(file)
const stripped = piexif.remove(dataUrl) // APP1 marker を除外
const blob = dataUrlToBlob(stripped)
4. PNG / WebP / HEIC は Canvas で再エンコード
これらの形式は EXIF が tEXt / iTXt / EXIF chunk のように混ざっていて、 ピンポイントで抜く実装は重い。 v1 では Canvas API で createImageBitmap → drawImage → toBlob を通して再描画してメタデータを全消去する戦略を採りました。 透過は維持。 画質は 95% で再エンコード (PNG は lossless なので品質パラメータは無視される)。
const bitmap = await createImageBitmap(file)
const canvas = document.createElement("canvas")
canvas.width = bitmap.width
canvas.height = bitmap.height
canvas.getContext("2d")!.drawImage(bitmap, 0, 0)
const blob = await new Promise<Blob>((res) => canvas.toBlob(res, mime, 0.95))
createImageBitmap() は WebP / PNG / JPEG / HEIC をブラウザ標準でデコードしてくれるので、 ライブラリ追加なしで形式横断のクリーン化が回ります。
5. GPS は別格で扱う
GPS が見つかったときは:
- マゼンタ枠の専用パネルで強調
- 緯度経度・高度を数値表示
- OpenStreetMap への mlat / mlon リンク (新規タブで開く)
- 「これが画像と一緒に公開されると位置情報が漏れる」 という直接的な警告文
UX 的に 「ここが一番危ない」 を視覚的に最優先で伝える設計にしています。
6. バッチ strip
「剥がして保存」 タブは複数ファイル同時投入対応。 ループで stripMetadata() を呼び、 進捗バーで何枚目を処理しているか表示。 各ファイルに個別の download ボタンを並べて、 必要な分だけ書き出せる作り。
苦労したところ
- exifr の dynamic import: 同梱の
defaultexport と namedparse関数がどちらでも呼べるパスを両方フォールバックしないと、 環境によって読めないケースがあった。exifr.parse ?? exifr.default?.parseで吸収。 - piexifjs の型定義:
@types/piexifjsは 1.0 系で実体は古い。import * as piexifModした後、defaultか module 直接かを実行時に分岐する形で型を当てた。 - HEIC → strip:
createImageBitmap()は HEIC を Safari / モバイル Chrome ではデコードできることが多いが、 デスクトップ Chrome は OS の HEIC コーデックに依存する。 失敗時はエラーリストに残してユーザーに見せ、 他ファイルの処理を止めない方針。
今後の拡張
- 選択削除: GPS だけ剥がして著作権は残す等のタグ単位コントロール
- RAW / DNG 対応: カメラRAW のメタデータも見られるように
- PNG textChunk / iTXt の細かい解析: 著作権 / コメント / プロファイル名等
- バッチ zip 出力: 大量画像をまとめて1ファイルで書き出す
- GPS のインラインマップ: OpenStreetMap タイルを直接描画
- SNS-safe プリセット: 「Twitter 推奨」 「Instagram 推奨」 で最低限剥がす要素を1クリックで設定
このサービスから言える事
ラボの 11本柱:
- voice-scribe / clip-cast / bg-snap / text-pluck / pdf-anvil / pixel-lift / pic-flip / mind-cell / beam-drop / word-warp / exif-peel
カテゴリの数:
- 認識 (voice-scribe / text-pluck / bg-snap)
- 変換 (clip-cast / pdf-anvil / pixel-lift / pic-flip / word-warp)
- 生成 (mind-cell)
- 転送 (beam-drop)
- インスペクション / プライバシー (exif-peel) ← 新
「ブラウザ完結で何ができるか」 の射程を、 画像の中身を覗いて剥がすところまで拡げた格好。 「写真を SNS に上げる前にとりあえずここに通す」 の習慣化を狙えると、 ラボの中で beam-drop / pic-flip / bg-snap への動線も自然に作れます。
Next Action
読んだあとに、そのまま使って確かめる。
この開発ログは ExifPeel をどう育てているかの記録です。読んだらそのままサービス本体へ戻って、価値を確かめられるようにしています。