HEIC を JPG にブラウザの中だけで変換する:写真をサーバーに送らない作りに直す

広告WordPress・PHP を学べる本

Amazonのアソシエイトとして、WordPress functions.phpは適格販売により収入を得ています。

写真がサーバーにたまり続けていた

iPhone の写真(HEIC)を JPG に変換するツールを見直していたときのことです。変換そのものはブラウザの中でできていたのですが、変換した JPG をわざわざサーバーにアップロードして保存し、そこからダウンロードさせる作りになっていました。

保存したファイルを消す仕組みはなかったので、使った人の写真がサーバーにたまり続けていました。しかも、アップロードを受け取る PHP は拡張子しか見ておらず、だれでも何でも置ける状態でした。写真には撮った場所などの情報が入っていることもあるので、預からずに済むなら預からないほうが安心です。

ブラウザの中で変換が終わっているなら、サーバーに送る必要はありません。そこで、変換から保存までをすべてブラウザの中で行う作りに直しました。

前の作りと直した作り

前の作り 直した作り
変換する場所 ブラウザ ブラウザ
サーバーに送るもの 変換した JPG なし
保存のしかた サーバーに置いたファイルをダウンロード ブラウザの中で作ったリンクから保存
サーバーに残るもの 使った人の写真(消す仕組みなし) なし
アップロードの受け口 拡張子だけを見て保存 受け口をなくした(410 を返す)

直した作り

heic2any をバージョンを固定して読み込む

変換には heic2any というライブラリを使っています。jsDelivr から読み込みますが、バージョンを書かないと、知らないうちに中身が変わることがあるので、0.0.4 に固定しました。

index.html(読み込みの部分)
<script src="https://cdn.jsdelivr.net/npm/heic2any@0.0.4/dist/heic2any.min.js"></script>

変換して、保存のリンクを作る

変換と保存の部分は、次のような形です(考え方を示すための例で、ツールのコードそのままではありません)。

app.js(変換と保存の部分・形の例)
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)がそのまま動くと、だれでもファイルを置ける状態が続いてしまいます。そこで、ファイルは残したまま、中身を「もうありません」と返すだけにしました。

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 が来てもエラーにしない

参考

関連機能

  1. WordPress でページキャッシュが効いていても閲覧数(PV)を数える:Ajax で数えて管理画面の一覧に出す

  2. WordPress パンくずリスト表示用のショートコード

  3. 表示前にフックして出力テキストを置換する

サイドバー

よく使うカスタマイズ

最近の記事

Profile

PAGE TOP