写真がサーバーにたまり続けていた
iPhone の写真(HEIC)を JPG に変換するツールを見直していたときのことです。変換そのものはブラウザの中でできていたのですが、変換した JPG をわざわざサーバーにアップロードして保存し、そこからダウンロードさせる作りになっていました。
保存したファイルを消す仕組みはなかったので、使った人の写真がサーバーにたまり続けていました。しかも、アップロードを受け取る PHP は拡張子しか見ておらず、だれでも何でも置ける状態でした。写真には撮った場所などの情報が入っていることもあるので、預からずに済むなら預からないほうが安心です。
ブラウザの中で変換が終わっているなら、サーバーに送る必要はありません。そこで、変換から保存までをすべてブラウザの中で行う作りに直しました。
前の作りと直した作り
| 前の作り | 直した作り | |
|---|---|---|
| 変換する場所 | ブラウザ | ブラウザ |
| サーバーに送るもの | 変換した JPG | なし |
| 保存のしかた | サーバーに置いたファイルをダウンロード | ブラウザの中で作ったリンクから保存 |
| サーバーに残るもの | 使った人の写真(消す仕組みなし) | なし |
| アップロードの受け口 | 拡張子だけを見て保存 | 受け口をなくした(410 を返す) |
直した作り
heic2any をバージョンを固定して読み込む
変換には heic2any というライブラリを使っています。jsDelivr から読み込みますが、バージョンを書かないと、知らないうちに中身が変わることがあるので、0.0.4 に固定しました。
<script src="https://cdn.jsdelivr.net/npm/heic2any@0.0.4/dist/heic2any.min.js"></script>
変換して、保存のリンクを作る
変換と保存の部分は、次のような形です(考え方を示すための例で、ツールのコードそのままではありません)。
const input = document.getElementById('file');
const list = document.getElementById('list');
const quality = document.getElementById('quality');
input.addEventListener('change', async () => {
// 1 枚ずつ順に変換する(まとめて変換するとメモリが足りなくなる)
for (const file of Array.from(input.files)) {
await convertOne(file);
}
});
async function convertOne(file) {
const li = document.createElement('li');
const status = document.createElement('span');
li.appendChild(status);
list.appendChild(li);
const baseName = file.name.replace(/\.[^.]+$/, '');
let blob;
if (file.type === 'image/jpeg' || /\.jpe?g$/i.test(file.name)) {
// iPhone がもう JPG にして渡してきたときは、そのまま保存できるようにする
blob = file;
status.textContent = file.name + ':もう JPG になっています';
} else {
try {
const result = await heic2any({
blob: file,
toType: 'image/jpeg',
quality: Number(quality.value)
});
blob = Array.isArray(result) ? result[0] : result;
status.textContent = file.name + ':変換しました';
} catch (e) {
status.textContent = file.name + ':変換できませんでした';
return;
}
}
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = baseName + '.jpg';
a.textContent = '保存する';
li.appendChild(a);
}
| 書き方 | 役目 |
|---|---|
for … of と await |
1 枚が終わってから次の 1 枚に進む。何枚も同時に変換すると、スマホではメモリが足りなくなる |
heic2any({ … }) |
HEIC を JPEG の Blob にする。quality は画質(0〜1)で、画面で選べるようにした |
Array.isArray(result) |
1 つのファイルに複数の画像が入っていると配列で返るので、先頭の 1 枚を使う |
URL.createObjectURL(blob) |
ブラウザの中のデータを指す URL を作る。サーバーには何も送らない |
a.download = baseName + '.jpg' |
元のファイル名のまま、拡張子だけ jpg にして保存する(IMG_1234.HEIC → IMG_1234.jpg) |
textContent |
ファイル名を画面に出すときに、HTML として読まれないようにする |
ファイル名は使う人が付けたものなので、innerHTML で入れると、名前に HTML のタグが入っていたときにそのまま動いてしまいます。textContent なら文字として出るだけなので、ファイル名を出すところはすべてこちらにしました。
前の受け口は 410 を返すだけにする
このツールは git で管理していて、push すると公開される流れですが、git からファイルを消しても、サーバーの上のファイルは消えずに残ります。前のアップロードの受け口(upload.php)がそのまま動くと、だれでもファイルを置ける状態が続いてしまいます。そこで、ファイルは残したまま、中身を「もうありません」と返すだけにしました。
<?php // 前の作りの受け口。今は何も受け取らない。 http_response_code( 410 ); header( 'Content-Type: text/plain; charset=UTF-8' ); echo 'Gone'; exit;
| 書き方 | 役目 |
|---|---|
http_response_code( 410 ) |
「この URL はもう使えない」ことを返す(404 より、なくしたことがはっきり伝わる) |
exit |
送られてきたデータには何もしないで終わる |
iPhone の実機で分かったこと
パソコンで確かめたあと、iPhone の実機でも試してみたところ、思っていたのと違う動きがありました。iPhone のブラウザで写真を選んだり、その場で撮ったりすると、iPhone が自動で JPG にしてから渡してくるのです。ファイル名も image.jpg のようになっていました。
最初は「HEIC 以外ははじく」作りにしていたので、iPhone で使うと、正しく写真を選んでもエラーに見えてしまいます。そこで、JPG が来たときは「もう JPG になっています」と出して、そのまま保存できるようにしました。HEIC のままのファイルを送ってくるのは、パソコンに取り込んだ写真を選んだときなどです。
確かめた結果
| 確かめたこと | 結果 |
|---|---|
| 4032×3024 の HEIC | 変換できた。保存したファイルの頭が ff d8 ff(JPEG のファイルの始まり) |
| 日本語の名前の HEIC | 変換できた。中身の頭は ff d8 ff |
| サーバーへの通信 | CSS と JS だけ。写真はサーバーに送られていない |
| iPhone の実機で写真を選ぶ | iPhone が JPG にして渡してきた(ファイル名は image.jpg) |
中身の頭の ff d8 ff は、JPEG のファイルが必ずこの並びで始まるというものです。拡張子だけ jpg になっていて中身が HEIC のまま、ということがないかを、ここで見ています。
気をつけること
URL.createObjectURLで作った URL は、ページを開いているあいだ有効です。枚数が多いときは、使い終わったものをURL.revokeObjectURLで放すとメモリが減らせます- 大きな HEIC をたくさん選ぶと、スマホでは変換に時間がかかります。1 枚ずつ順に進めて、どこまで終わったかを画面に出すようにしました
- CDN からライブラリを読むときは、バージョンを固定しておきます
まとめ
- ブラウザの中で変換できているなら、写真をサーバーに送らずに保存までできる
- heic2any で変換し、
URL.createObjectURLで保存のリンクを作る。ファイル名はtextContentで出す - git で消したファイルはサーバーに残るので、前の受け口は 410 を返すだけにする
- iPhone のブラウザは写真を JPG にして渡してくるので、JPG が来てもエラーにしない


