AI Dev Lab
ExifPeel

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

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

·decision改善·stage公開中

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,
})

撮影日時のような DateTimeOriginalDate オブジェクトとして返ってくるので、 表示時には 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: 同梱の 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 →
ToonCast

ToonCast ができるまで — AnimeGANv2 をブラウザで動かす

AnimeGANv2 の小さな ONNX (約9MB) を onnxruntime-web (単一スレッド WASM=COOP/COEP不要、 color-revive で承認済みライブラリの再利用) で実行。 512x512・[-1,1] 正規化で推論し、 結果を元解像度に戻して表示する設計記録。 写真は端末内処理。

read log →
ColorRevive

ColorRevive ができるまで — onnxruntime-web で白黒写真をカラー化

DeOldify の量子化 ONNX を onnxruntime-web (CDN side-load・単一スレッド WASM=COOP/COEP不要) で実行。 256x256 でモデル推論し、 輝度は元写真・色だけ AI を YCbCr で再合成して輪郭を保つ設計記録。 写真は端末内処理。

read log →
PhotoTwin

PhotoTwin ができるまで — CLIP画像埋め込みで似た写真を見つける

CLIP (Xenova/clip-vit-base-patch32) の image-feature-extraction を transformers.js の CDN ESM で side-load し、 各写真を正規化ベクトル化。 cosine 類似度で重複・似た写真をブラウザ内で検出する設計記録 (新ライブラリ追加なし=what-cam と同じ CLIP の再利用)。

read log →
AkinFind

AkinFind ができるまで — 文章embeddingsで意味検索をブラウザ内に

多言語の文章埋め込みモデル (Xenova/multilingual-e5-small) を transformers.js の CDN ESM で side-load し、 各文を正規化ベクトル化。 cosine 類似度で意味検索と似ている文ペア検出を全て端末内で行う設計記録。

read log →
WhatCam

WhatCam ができるまで — CLIP のゼロショット画像分類をブラウザで動かす

CLIP (Xenova/clip-vit-base-patch32) を transformers.js の CDN ESM で side-load し、 写真と候補ラベルの近さをブラウザ内で計算。 日本語ラベルを英語プロンプトに変換し、 図鑑と自由入力の両モードで「これ何?」を判定する設計記録。

read log →
DepthCast

DepthCast ができるまで — 1枚の写真をAIの深度推定で立体にする

Depth Anything (transformers.js) を CDN ESM で side-load し、 1枚の写真から深度マップを推定。 WebGL2 フラグメントシェーダで深度に比例した視差 (iterative backward parallax) を作り、 赤青アナグリフ / WebM 書き出しまで端末内で完結させた設計記録。

read log →