💥 重い、そして不安 — 「サイトが激重っ!?」
キレイな写真ほど、データが大きく"重く"なりがち。かといって手軽なオンライン変換サイトには、別の心配ごとがありました。
選択肢A:オンライン変換サイト
- お手軽だが写真を相手のサーバーに送信
- → 人に見せたくない写真は不安
選択肢B:手元で完結する道具
- 変換は自分のPCの中だけ・写真は外に出ない
- → 安心、オフラインでも動く
ぼくサイトがモッサモサで、開くのを待つ間に1コ歳をとりそう……。かといって家族写真を知らないサイトに上げるのは、もっとイヤ。八方ふさがりだ〜!
クロード落ち着いて相棒、その悩み、両方いっぺんに片づくよ。変換はブラウザの中だけでできる——つまり写真は一歩も外に出ない。世の中に無い道具は、作っちゃえばいいのさ。
🛠 AIと作る、そして🐛 HEIC事件
設計のキモは「送らない」「フォルダごと一発」。完成——と思いきや、一部だけ変換されない!?
// 写真をCanvasに描き直す(ぜんぶブラウザの中だけ)
const bmp = await createImageBitmap(file);
ctx.drawImage(bmp, 0, 0);
// 形式と品質を指定して書き出す(メタデータもここで消える)
canvas.toBlob(blob => { /* 軽くなった画像 */ }, 'image/webp', 0.8);
ぼくJPEGはスッと軽くなったのに、何枚かはウンともスンとも言わない……。ぼくのコード、ついにバグった!?(半泣き)
クロードそれ冤罪。真犯人はHEIC——ブラウザによっては読めない、ちょっと頑固な形式なんだ。だから読めなかった写真は「失敗」と正直に表示して、「iPhoneはJPEGで撮ると確実だよ」と逃げ道まで添えよう。だまって見捨てないのが、いい道具。
🛡 "親切な失敗"に。 読めなかった写真は名前つきで「失敗」と表示し、「iPhoneは設定→カメラ→フォーマットを『互換性優先(JPEG)』に」と添える。エラーを隠さず、解決の道筋まで見せる。
🎉 完成、そして進化版『Snapfit』へ
フォルダごと一気に軽量化。毎日使ううちに機能を足し、名前も中身も別物に育った。
「重すぎる」から、
「フォルダごと一発」へ。
ぼく見て見て!十数MBが数百KBに、フォルダ100枚が一瞬で片づいた!ぼく、もしかして天才……?
クロード天才はたぶん相棒(=ぼく)のほうだけど……まあ、いい仕事したね。写真は1枚も外に出てないのも忘れずに。せっかくだから、もっと気の利く道具に育てよう。
🆕 進化版 Snapfit でできること(最新版)
- フォルダ一括+並列処理。 何百枚でもまとめて、複数同時に高速変換(裏側のWeb Workerで画面が固まらない)。
- WebP / JPEG / PNG。 長辺サイズ(〜3840px)と品質をスライダーで指定。対応ブラウザではAVIFも選べます。
- 🎯 目標サイズに収める。 「200KBまで」のように上限を決めると、品質を自動調整してそのサイズ内に収めます。
- 🛡 メタデータ削除(既定ON)。 GPS位置情報・撮影日時・カメラ機種を除去し、消した項目をバッジで表示。Web公開で"撮影場所"が漏れる事故を防ぐ。
- 🔍 変換前後の比較&並べ替え。 サムネをクリックで拡大し、変換前後を見比べ。結果は削減率やサイズ順に並べ替えできます。
- 🌗 ダークモード&日本語/英語。 画面右上のボタンで切り替え(設定は記憶)。海外の人にもそのまま使えます。
- 💾 2通りの保存。 「ZIPでダウンロード」か、対応ブラウザなら「デスクトップに保存」でフォルダへ直接書き出し(構造も再現)。
- 気くばり。 EXIF回転を自動補正、同名は自動リネームで上書き防止、途中キャンセルも可。
🔒 変わらない約束。 機能が増えても、処理はすべてあなたのブラウザの中だけ。元の写真も、消したメタデータも、どこにも送られません。
使い方 — Snapfitを7ステップで
画面を開いてから保存までを、7ステップで解説します。下のパネルは Snapfitの実画面を忠実に再現したものです。
まず画面を開く(全体像)
Snapfitを開くと、必要なものはこの1画面だけ。上から「読み込み → 設定 → 変換 → 保存」が一直線に並びます。変換前なので、保存系のボタンはグレー(まだ押せない状態)です。
Snapfit
写真を、Web用にフィットさせる。
iPhoneやデジカメの画像をフォルダごと一括変換。すべてブラウザ内で処理され、画像はネットに送信されません。
🔒 Snapfitは、このパソコンのブラウザ内だけで動作します。画像が外部に送信されることは一切ありません。
ここに画像をドロップ
または
対応: JPEG / PNG / WEBP / GIF / BMP。HEIC/HEIFは対応ブラウザのみ。
▲ ① Snapfitのメイン画面
画像ファイル/フォルダを選んで"許可"する
軽量化したい画像ファイルやフォルダを選びます。フォルダのときはブラウザが確認を出しますが、「アップロードしますか?」はブラウザ共通の文言。Snapfitは画像を読み込むだけで、ネットには一切送りません。「アップロード」を押して読み込みます。
ここに画像をドロップ
または
2 個のファイルをこのサイトにアップロードしますか?
「PC電源画像」のすべてのファイルがアップロードされます。この操作は、サイトを信頼できる場合にのみ行ってください。
▲ ④ フォルダ選択時にブラウザが出す確認("アップロード"でも写真は外に出ません)
長辺サイズを決める
写真を「表示に必要な大きさ」まで縮小します。ブログ本文なら 1600 px(ブログ向け)、迷ったら 1920 px(推奨)。元より大きくは引き伸ばしません。
▲ ③ 長辺サイズのメニュー(1600 px を選択中)
出力(ファイル)形式を選ぶ
Web用途なら WEBP(推奨)がいちばん軽い。写真は WEBP か JPEG、透過や図版・スクショは PNG(可逆)を選びます。品質は既定 0.80(写真は0.75〜0.85が黄金比)。
▲ ② 出力形式のメニュー(WEBP を選択中)
変換を実行して保存する
「変換開始」を押すと、複数の写真を並列で一気に変換。削減率のサマリーが出ます(下の例は合計 −98.4%、メタデータも削除済み)。受け取りは「ZIPでダウンロード」か「デスクトップに保存」。ここでは「デスクトップに保存」を押します。
変換が完了しました。2 / 2 件を出力しました。
合計: 13.6 MB → 223.4 KB (−98.4%)
▲ ⑤ 変換結果(合計 −98.4%・メタデータ削除済み)
デスクトップに新しいフォルダが作られる
「デスクトップに保存」を押すと、保存場所を選ぶ画面(フォルダ選択ダイアログ)が出ます。デスクトップなど保存したい場所を選んで「フォルダーの選択」を押すと、その中に 「軽量化成功!」という新しいフォルダが自動で作られ、そこに画像がまとめて保存されます。(対応:Chrome / Edge)
▲ ⑥ デスクトップに新しいフォルダが自動でできる
「軽量化成功!」フォルダに保存完了
できあがるのが 「軽量化成功!」フォルダ。この中に、GPS等の個人情報を消した・軽くなった画像が、元のフォルダ構成のまま保存されます。お疲れさまでした!
▲ ⑦ デスクトップにできる「軽量化成功!」フォルダ(中に軽量化済み画像が入る)
💡 こんな時はこの設定(早見表)
- ブログ本文の写真 → WEBP / 品質 0.80 / 長辺 1600 px
- ポートフォリオ・作品紹介 → WEBP / 品質 0.85 / 長辺 1920 px
- SNSアイコン・サムネ → WEBP / 品質 0.80 / 長辺 1280 px
- 図版・スクショ・透過素材 → PNG(可逆)/ 長辺 変更しない
💻 どこで使える?(対応環境)
- ✅ 基本(読み込み→変換→ZIP保存) … Windows・Mac・Linux、さらにスマホでもOK。Chrome・Edge・Safari・Firefox など主要ブラウザで動きます。Windows専用でもChrome専用でもありません。
- ✅ WebP / JPEG / PNG 出力 … 主要ブラウザすべてに対応。
- ⚠ 「デスクトップに保存」(フォルダへ直接書き出し) … Chrome / Edge(Chromium系)が必要。Safari・Firefoxではこのボタンが自動で消え、代わりにZIP保存が使えます(できないわけではありません)。
- ⚠ HEIC / HEIF(iPhoneの標準写真)の読み込み … Mac・iPhoneのSafariは可、Chromeは不可。iPhoneは「設定 → カメラ → フォーマット」を「互換性優先(JPEG)」にすると確実です。
- 💡 共通 … インストール不要・登録不要・無料。ページを開くだけで使えます。
💡 こんなときに・こんな人に使える
ひとつでも当てはまったら、Snapfitの出番です。
🎯 こんなときに使える(具体例)
- ブログ記事やSNS投稿に写真を載せる前に、表示が重くならないよう軽くしたい
- ホームページ・ポートフォリオの読み込みを速くして、離脱を減らしたい
- メルカリやネットショップの商品写真を、フォルダごと一括で縮小したい
- メールやLINE・チャットで送る写真の容量を下げたい
- SNSやブログで公開する前に、写真からGPS(撮影場所)を消したい
- iPhoneのHEIC写真を、JPEG / WebP にそろえて扱いやすくしたい
- 旅行やイベントで撮った数百枚を、一気にまとめて軽量化したい
🙆 こんな人に使える
- ブログ・サイト運営者、個人クリエイター
- ネットショップ・フリマ出品者
- 旅行・イベント・子どもの写真など、写真をたくさん撮る人
- プライバシーが気になる人(家族や自宅が写る写真を扱う人)
- 「知らない変換サイトに写真を上げたくない」人
- パソコンが得意でなくても、ボタン操作だけで済ませたい人
🔒 共通して安心。 どの使い方でも、写真はあなたのブラウザの中だけで処理され、ネットには送られません。無料・登録不要・インストール不要です。
🔬 しくみと技術 — 何で、どう動くのか
魔法ではありません。サーバーもインストールも要らない、いわば"ただのWebページ"。そのシンプルな仕組みを、4ステップで見てみましょう。
フォルダをまるごと選択。中の画像ファイルだけを自動で拾い上げる。
各写真をブラウザの Canvas に一度描く。すべて手元で完結し、写真はネットに出ない。
toBlob でWebP/JPEG/PNGへ再エンコード。この時点でEXIF等のメタデータも消える。
1枚ずつ、ZIP一括、またはフォルダへ直接保存。元の写真には手を触れない。
🧩 何でできている?(やさしい技術メモ)
- 特別な言語はいりません。 中身は、Webページと同じ HTML・CSS・JavaScript の3つだけ。大がかりな"開発の道具一式"は使わず、ファイルは実質3個+ZIPを作る小さな部品(JSZip)のみ。
- ブラウザの"もとからある機能"をフル活用。 画像を読む・小さくする・別の形式で保存する——これらは今どきのブラウザに最初から備わっています。ふだんはサーバー(どこか別のコンピュータ)がやる仕事を、あなたのブラウザが肩代わりしているわけです。
- あえてサーバーを持たない設計。 だから写真が外に出ず、置き場所代もタダ。インストールも不要で、ページを開けばすぐ動きます。弱点は、変換の"力仕事"にあなたのPCのパワーを使うこと(数千枚なら何回かに分けると安心)。
⚠ 使い方のおやくそく。 自分が撮った・正当に利用できる写真の軽量化に使ってください。他人の著作物を無断で加工・再配布するのはNG。HEICはブラウザによって読めない場合があります(iPhoneは「互換性優先(JPEG)」が確実)。
🧪 コード詳解 — Snapfitは実際どう書かれているか
気になる人向けの"中身"。実際の app.js を抜粋しながら、6つの山場を解説します(コードは読みやすく一部を簡略化しています)。
🔬 コードを詳しく見る
🗂 全体像 — たった3ファイル+1ライブラリ
- index.html — 画面(ドロップ欄・出力形式・品質・サイズ・チェック・各ボタン)。
- style.css — 見た目だけ。レイアウトと配色。
- app.js — 頭脳(約670行)。受け取る → 並列で変換 → 保存を担当。
- JSZip(MIT)— ZIPを作る部分だけを任せる外部ライブラリ。これ以外に依存なし。
🧭 サーバー側のコードは1行もありません。 下のコードはすべて"あなたのブラウザの中"で動きます。だから写真が外に出ません。
① 画像を受け取る — フォルダを"再帰"でたどる
フォルダがドロップされたら、中のサブフォルダまで潜って画像だけを集めます。あとで構造を再現するため、各ファイルに相対パス(webkitRelativePath)を覚えさせます。
async function walkEntry(entry, prefix, out) {
if (entry.isFile) {
const file = await new Promise(res => entry.file(res));
// フォルダ構造を後で再現するため、相対パスを覚えさせる
Object.defineProperty(file, 'webkitRelativePath',
{ value: prefix + file.name });
out.push(file);
} else if (entry.isDirectory) { // フォルダなら中へ潜る
const reader = entry.createReader();
for (const child of await readAllEntries(reader))
await walkEntry(child, prefix + entry.name + '/', out);
}
}
② 並列処理 — CPUコア数ぶん"同時"に変換する
1枚ずつ順番に処理すると遅い。そこで CPUのコア数を見て最大4枚を同時に走らせる「ワーカープール」を組みます。完了は順不同になるので、先頭から順に画面へ流す(flush)工夫で表示順を保ちます。
// CPUコア数を見て、最大4枚を"同時"に変換
const CONCURRENCY = Math.min(4, navigator.hardwareConcurrency || 4);
const worker = async () => {
while (true) {
if (cancelRequested) return; // 途中キャンセルに対応
const i = next++; // 次の1枚を取る
if (i >= total) return;
try { results[i] = { ok:true, data: await convertImageFile(files[i]) }; }
catch (e) { results[i] = { ok:false, err:e, name: files[i].name }; }
progressBar.value = Math.round(++done / total * 100);
flush(); // 先頭から順に画面へ反映
}
};
// ワーカーを CONCURRENCY 本だけ並走させ、全部終わるまで待つ
await Promise.all(pool);
③ 1枚を変換する核心 — デコード→リサイズ→書き出し
変換の心臓部はわずか数行。createImageBitmap がデコードとEXIF回転の補正まで肩代わりし、Canvasに描き直して toBlob で再エンコードします。JPEGは透過部分が黒くなるのを防ぐため、先に白で塗ります。
// ① デコード+EXIF回転の補正をブラウザに任せる
const bitmap = await createImageBitmap(file, { imageOrientation: 'from-image' });
// ② 長辺を maxEdge まで縮小(拡大はしない)
const scale = Math.min(1, maxEdge / Math.max(bitmap.width, bitmap.height));
canvas.width = Math.round(bitmap.width * scale);
canvas.height = Math.round(bitmap.height * scale);
// ③ Canvasへ描き直す(JPEGは透過対策に白背景を敷く)
if (format === 'jpeg') { ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, canvas.width, canvas.height); }
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
// ④ 指定形式で書き出す(=再エンコード=メタデータが消える瞬間)
canvas.toBlob(blob => { /* 軽くなった1枚 */ }, mimeType, quality);
④ メタデータが消えるしくみ+"何を消したか"を見せる
Canvasに描き直した時点で、コピーされるのはピクセルだけ。GPSや撮影日時などのEXIFは原理的に残りません。ただ「消えた」だけでは不親切なので、元のJPEGからEXIFを先に読み取り、削除した項目をバッジで見せています。
// JPEG先頭の APP1(0xFFE1) セグメントから "Exif" を探して読む
if (view.getUint16(offset) === 0xFFE1 && isExifSignature(view, offset + 4)) {
return parseExifIfd(view, offset + 10, …); // タグ番号 → 日本語ラベルへ変換
}
// 0x8825=GPS位置情報 / 0x9003=撮影日時 / 0x0110=カメラモデル / 0x010F=メーカー …
🛡 「削除」と「表示」は別の仕事。 削除は再エンコードが自動で行い、表示用にだけ元データを読みます。読み取りも保存も、もちろんブラウザの中だけ。
⑤ 2通りの保存 — ZIP と「フォルダへ直接」
まとめて受け取る方法は2つ。JSZipで1つのZIPにする道と、対応ブラウザの File System Access API でデスクトップに「軽量化成功!」フォルダを作って直接書き込む道です。
// (1) まとめてZIPにする
const zip = new JSZip();
convertedFiles.forEach(f => zip.file(ensureUnique(used, f.zipPath), f.blob));
triggerDownload(await zip.generateAsync({ type: 'blob' }), 'converted-images.zip');
// (2) デスクトップへ直接書き出す(対応ブラウザのみ)
const parent = await showDirectoryPicker({ mode: 'readwrite', startIn: 'desktop' });
const dir = await parent.getDirectoryHandle('軽量化成功!', { create: true });
const handle = await dir.getFileHandle(fileName, { create: true });
const w = await handle.createWritable();
await w.write(blob);
await w.close();
⑥ 地味だけど効く"安全と堅牢"の小ワザ
- メモリ解放。 プレビュー用に作ったURLは
Setで管理し、revokeObjectURLで回収。離脱時(beforeunload)にも掃除。 - 上書き防止。 同名が出たら
ensureUniqueが「name (1).webp」と自動リネーム。 - 親切な失敗。 読めない画像(HEIC等)は黙って落とさず、名前つきで「失敗」と表示——あの"HEIC事件"の教訓。
- 安全な描画。
'use strict'+ 表示は全部textContent(HTML文字列を組み立てない=XSS耐性)。配布側もCSP厳格・インラインJS無し。
👀 全文はあなたの目で確認できます。 Snapfitのページで右クリック →「ページのソースを表示」すれば、app.js の中身がそのまま読めます。隠し事はありません。
🌙 教訓 — 「重い・面倒・不安」は、道具にできる
📌 この一件で学んだこと
- 「重い」には理由がある。 縮小+再圧縮で、見た目はほぼそのまま軽くなる。
- 便利さと安心は両立できる。 ブラウザの中だけで処理すれば、写真も個人情報も外に出さずに済む。
- 繰り返す作業は、道具にする。 1枚ずつを"フォルダごと一発"に変える価値は大きい。
- 失敗は隠さず見せる。 読めないHEICは「失敗」と表示し、次の一手まで案内する。
このサイト「AI探検隊」は、AIで「Webサイト・音楽・画像」を実際につくって見せる場所です。今回のような小さな"自作ツール"も、AIと組めば週末の片手間で生まれます。「重いな」「面倒だな」「ちょっと不安だな」——そう思った瞬間こそ、道具づくりのチャンスです。