▍AI Dev Lab
ExifPeel

ExifPeel ができるまで — ブラウザだけで画像のEXIFを確認・剥離する設計

exifr (EXIF パーサ) と piexifjs (JPEG EXIF 操作) と Canvas API をブラウザ内で動かし、 GPS / 撮影機種 / 編集ソフト履歴を覗いて剥がすツールの設計記録。 JPEG ロスレス strip と PNG/WebP の Canvas 再エンコード戦略、 GPS 警告 UI まで解説します。

·decision改善·stage公開中

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 不要で、 で組み合わせれば一切アップロードなしのメタデータ検査 + 剥離が成立する。 ラボ ジャンル にそのまま乗ります。

#見た目の方針

フォレンジック・クロム × ウラニウム・グロウ。

  • 深いスレート (#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 の数値プロパティに正規化してくれるので、 後段の判定が楽。

ts
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 で マーカーごと抜くだけで完了。 画素データ自体は触らないので 画質劣化ゼロ。

ts
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 なので品質パラメータは無視される)。

ts
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: 同梱の default export と named parse 関数がどちらでも呼べるパスを両方フォールバックしないと、 環境によって読めないケースがあった。 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 を実際に動かす。

この開発ログは ExifPeel をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ShadowTwin

立体パズルの別解を作る:同じ影と最少の積み木

ShadowTwinの開発記録。3方向の図から別々の最少配置を2つ探す仕組み、5問の独立全探索、スマホの横並び比較、保存・再編集と入力検査を解説します。実照明の計算や能力診断ではありません。

read log →
SlitCard

スリットアニメーションを作る:4コマの合成と透明原稿の設計

SlitCardの開発記録。4コマの帯を交互に合成し、すき間と黒帯で別のコマを見せる仕組みを解説。同縮尺の台紙・透明シートSVG、50mm確認線、手動プレビュー・初期非表示、再編集と実出力検査を整理します。

read log →
WindowLight

日当たりシミュレーションを作る:窓の光を床へ投影する幾何モデル

WindowLightの開発記録。太陽位置近似から窓の4隅を床へ投影し、凸多角形を部屋で切り取る設計を解説。国立天文台の16時刻との限定照合、大気差・低高度・実際の採光との違い、SVGと設定の再編集を整理します。

read log →
GridLights

電源構成の比較を作る:割合と実量を100個の電球で読み解く

電源構成比較GridLightsの開発記録。12か国の年次発電データを最大剰余法で100個の電球に変換し、割合とTWhを比較する設計を解説。欠損を0補完しない7分類、予想と実値、出典付きSVGと実データ検証を整理します。

read log →
SoundLoom

音のお絵かきを作る:実WAVから絵を測り直す設計

音のお絵かきの開発記録。描画を正弦波のPCMとWAVへ変換し、保存音声の周波数をGoertzel法で測って比較する仕組みを解説。音量・測定窓・量子化の限界と、編集・離脱時の古い結果を抑止する検証を整理します。

read log →
MazeLoom

宝探し迷路を作る:寄り道まで含めた最短ルートの探索

宝探し迷路の開発記録。通路を木として生成し、現在地と3つの宝の取得状態を組み合わせて最短ルートを探す仕組みを解説。問題コードの版固定、画面と解答付きPDFの一致、手数と印刷の検証を整理します。

read log →