PHP

爆速!PHPでJPG/PNG画像をWebP変換&ドラッグ&ドロップ対応アップローダーを構築する

Webサイトのパフォーマンス向上に不可欠なWebP画像。しかし、既存のJPGやPNG画像をまとめて変換するのは手間がかかりますよね?この記事では、PHPを使って、JPG/PNG画像をWebPに変換し、さらにドラッグ&ドロップで手軽にアップロードできるツールを構築する方法を、コピペで使えるコードと共に徹底解説します。

画像最適化に悩むWebエンジニア、デザイナー、ブロガーの皆さん、必見です!

なぜ今、WebPなのか?

WebPはGoogleが開発した画像フォーマットで、従来のJPGやPNGに比べて、同等またはそれ以上の画質で圧倒的にファイルサイズを小さくできるという特徴があります。これにより、Webページの読み込み速度が大幅に向上し、ユーザーエクスペリエンスの改善、SEO評価の向上、サーバーの帯域幅削減といった多岐にわたるメリットが得られます。

  • ファイルサイズの削減: JPGに比べて約25-34%、PNGに比べて約26%のファイルサイズ削減が期待できます。
  • 透過とアニメーション対応: PNGのような透過(アルファチャネル)やGIFのようなアニメーションにも対応しています。
  • 主要ブラウザのサポート: 現在、主要なほとんどのモダンブラウザ(Chrome, Firefox, Edge, Safariなど)がWebPをサポートしています。

開発環境の準備

このツールを動作させるために必要なものは以下の通りです。

  • PHPが動作するWebサーバー: Apache, Nginxなど。
  • PHP GDライブラリ: 特にWebPを扱うためには、PHPのGDライブラリがWebPサポート付きでコンパイルされている必要があります。

PHP GDライブラリの確認と有効化

GDライブラリが有効かどうか、WebPがサポートされているかを確認するには、簡単なPHPファイルでphpinfo()を実行するのが手っ取り早いです。

`info.php`
<?php phpinfo(); ?>

このファイルをサーバーに置いてブラウザでアクセスし、「GD」セクションを探してください。「WebP Support」が「enabled」になっていればOKです。もし無効になっている場合は、php.iniファイルを編集してGD拡張を有効にし、WebPサポートを追加する必要があります。

`php.ini`の例
;extension=gd  <-- セミコロンを外して有効化
extension=gd

;GDライブラリがWebPをサポートするように設定 (PHPのバージョンや環境により不要な場合もあり)
; 通常はgd有効化でWebPも自動で有効になります。

変更後、Webサーバーを再起動することを忘れないでください。

コピペでOK!オールインワンコード

それでは、早速メインのコードをご紹介します。このコードは単一の.phpファイルとして動作し、HTML、CSS、JavaScript、そしてPHPのバックエンド処理がすべて含まれています。

1. `index.php` ファイルを作成

以下のコードをコピーし、index.phpという名前で保存してください。

`index.php`
<?php

// エラー報告を有効にする(開発時のみ)
// ini_set('display_errors', 1);
// ini_set('display_startup_errors', 1);
// error_reporting(E_ALL);

$message = '';
$downloadLink = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['image'])) {
$targetDir = 'uploads/';
if (!is_dir($targetDir)) {
mkdir($targetDir, 0777, true);
}

$uploadFile = $targetDir . basename($_FILES['image']['name']);
$imageFileType = strtolower(pathinfo($uploadFile, PATHINFO_EXTENSION));

// 許可する画像形式
$allowedTypes = ['jpg', 'jpeg', 'png'];

if (in_array($imageFileType, $allowedTypes)) {
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadFile)) {
$sourceImage = null;
if ($imageFileType === 'jpg' || $imageFileType === 'jpeg') {
    $sourceImage = imagecreatefromjpeg($uploadFile);
} elseif ($imageFileType === 'png') {
    $sourceImage = imagecreatefrompng($uploadFile);
    // PNGの透過情報を保持するために、アルファブレンディングを無効にする
    imagealphablending($sourceImage, false);
    imagesavealpha($sourceImage, true);
}

if ($sourceImage) {
    $outputFileName = pathinfo($uploadFile, PATHINFO_FILENAME) . '.webp';
    $outputFilePath = $targetDir . $outputFileName;

    // WebPに変換
    // 変換品質 (0-100, 100が最高品質)
    $quality = 80;
    if (imagewebp($sourceImage, $outputFilePath, $quality)) {
        $message = '画像をWebPに変換しました!';
        $downloadLink = $outputFilePath;
    } else {
        $message = 'WebPへの変換に失敗しました。';
    }
    imagedestroy($sourceImage);
} else {
    $message = 'アップロードされた画像の読み込みに失敗しました。';
}
} else {
$message = 'ファイルのアップロードに失敗しました。';
}
} else {
$message = 'JPGまたはPNG画像のみアップロードできます。';
}

// アップロードされた元のファイルを削除(必要に応じて)
// unlink($uploadFile);
}

?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>画像WebP変換ツール</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background-color: #f0f2f5;
color: #333;
}
.container {
background-color: #ffffff;
padding: 40px;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
text-align: center;
width: 100%;
max-width: 500px;
}
h1 {
color: #007bff;
margin-bottom: 30px;
font-size: 2em;
}
form {
display: flex;
flex-direction: column;
gap: 20px;
}

/* ドラッグ&ドロップエリアのスタイル */
.drop-area {
border: 3px dashed #ccc;
padding: 30px;
border-radius: 10px;
background-color: #f9f9f9;
cursor: pointer;
transition: border-color 0.3s ease, background-color 0.3s ease;
text-align: center;
font-size: 1.2em;
color: #666;
margin-bottom: 20px; /* ファイル入力との間隔 */
}
.drop-area.highlight {
border-color: #007bff;
background-color: #e0f2ff;
}
.drop-area p {
margin: 0;
}
.drop-area .or-text {
font-size: 0.9em;
color: #999;
margin: 10px 0;
}


input[type="file"] {
display: none; /* 元のファイル入力は非表示にする */
}
.file-select-button {
background: #007bff;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
outline: none;
transition: background-color 0.3s ease;
display: inline-block; /* ボタンとして表示 */
margin-top: 10px;
}
.file-select-button:hover {
background-color: #0056b3;
}
.file-name-display {
margin-top: 10px;
font-size: 0.9em;
color: #555;
min-height: 1.2em; /* ファイル名がない時に領域を確保 */
}

input[type="submit"] {
background-color: #28a745;
color: white;
padding: 12px 25px;
border: none;
border-radius: 5px;
font-size: 1.1em;
cursor: pointer;
transition: background-color 0.3s ease;
}
input[type="submit"]:hover {
background-color: #218838;
}
.message {
margin-top: 25px;
font-size: 1.1em;
color: #333;
min-height: 30px; /* メッセージ表示領域の確保 */
}
.success {
color: #28a745;
font-weight: bold;
}
.error {
color: #dc3545;
font-weight: bold;
}
.download-link {
margin-top: 20px;
}
.download-link a {
display: inline-block;
background-color: #17a2b8;
color: white;
padding: 10px 20px;
border-radius: 5px;
text-decoration: none;
font-size: 1.1em;
transition: background-color 0.3s ease;
}
.download-link a:hover {
background-color: #138496;
}
</style>
</head>
<body>
<div class="container">
<h1>JPG/PNG to WebP変換</h1>
<form action="" method="post" enctype="multipart/form-data">
<div id="dropArea" class="drop-area">
    <p>ファイルをここにドラッグ&ドロップ</p>
    <p class="or-text">— または —</p>
    <label for="imageInput" class="file-select-button">ファイルを選択</label>
    <input type="file" name="image" id="imageInput" accept=".jpg, .jpeg, .png" required>
    <div class="file-name-display" id="fileNameDisplay">選択されていません</div>
</div>

<input type="submit" value="WebPに変換">
</form>

<div class="message">
<?php if (!empty($message)): ?>
    <p class="<?php echo (strpos($message, '成功') !== false) ? 'success' : 'error'; ?>"><?php echo $message; ?></p>
<?php endif; ?>
</div>

<?php if (!empty($downloadLink)): ?>
<div class="download-link">
    <a href="<?php echo htmlspecialchars($downloadLink); ?>" download>ダウンロード</a>
</div>
<?php endif; ?>
</div>

<script>
const dropArea = document.getElementById('dropArea');
const fileInput = document.getElementById('imageInput');
const fileNameDisplay = document.getElementById('fileNameDisplay');
const fileSelectButton = document.querySelector('.file-select-button');

// ドラッグイベントのデフォルト動作を停止
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false); // 全体でのドロップを防ぐ
});

function preventDefaults (e) {
e.preventDefault();
e.stopPropagation();
}

// ドラッグ中のスタイル変更
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, () => dropArea.classList.add('highlight'), false);
});

['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, () => dropArea.classList.remove('highlight'), false);
});

// ドロップ時の処理
dropArea.addEventListener('drop', handleDrop, false);

function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;

if (files.length > 0) {
    const file = files[0];
    const allowedTypes = ['image/jpeg', 'image/png']; // 許可するMIMEタイプ

    if (allowedTypes.includes(file.type)) {
        // DataTransferオブジェクトからFileListを作成し、inputにセット
        const dataTransfer = new DataTransfer();
        dataTransfer.items.add(file);
        fileInput.files = dataTransfer.files;

        fileNameDisplay.textContent = file.name; // ファイル名を表示
    } else {
        alert('JPGまたはPNG画像のみアップロードできます。');
        fileNameDisplay.textContent = '選択されていません';
        fileInput.value = ''; // 不正なファイルをクリア
    }
}
}

// ファイル選択ボタンがクリックされた時の処理 (input[type="file"]に委譲)
fileSelectButton.addEventListener('click', () => {
fileInput.click();
});

// 通常のファイル入力が変更された時の処理
fileInput.addEventListener('change', function() {
if (this.files.length > 0) {
    const file = this.files[0];
    const allowedTypes = ['image/jpeg', 'image/png'];

    if (allowedTypes.includes(file.type)) {
        fileNameDisplay.textContent = file.name;
    } else {
        alert('JPGまたはPNG画像のみアップロードできます。');
        fileNameDisplay.textContent = '選択されていません';
        this.value = ''; // 不正なファイルをクリア
    }
} else {
    fileNameDisplay.textContent = '選択されていません';
}
});

// ページロード時の初期表示
document.addEventListener('DOMContentLoaded', () => {
fileNameDisplay.textContent = '選択されていません';
});

</script>
</body>
</html>

2. `uploads`ディレクトリの作成

index.phpと同じ階層に、**uploads** という名前のディレクトリを作成してください。このディレクトリに変換後のWebPファイルが保存されます。

コマンド例
mkdir uploads
chmod 777 uploads  # 開発用。本番環境ではより厳密な権限設定を!

🚨 セキュリティに関する重要事項 🚨

uploadsディレクトリに777(すべてのユーザーが読み書き実行可能)権限を与えるのは、開発環境でのみにしてください。本番環境では、ウェブサーバーが書き込み可能な最小限の権限(例: ApacheユーザーやNginxユーザーにのみ書き込み権限を与える)を設定し、実行権限は絶対に与えないようにしてください。悪意のあるスクリプトがアップロードされた場合に実行されるリスクを最小限に抑えるためです。

3. Webサーバーへの配置とアクセス

index.phpuploadsディレクトリを、お使いのWebサーバーの公開ディレクトリ(例: ApacheのhtdocsやNginxのhtmlなど)に配置します。

その後、Webブラウザでindex.phpにアクセスしてください。例えば、ローカル環境であれば http://localhost/index.php のようになるでしょう。

コードの技術解説

このツールがどのように機能しているのか、HTML、CSS、JavaScript、PHPそれぞれの役割を詳しく見ていきましょう。

HTML: 直感的UIの構造

ユーザーが画像をアップロードし、結果を確認するためのインターフェースを提供します。

  • フォーム (`<form>`):

    enctype="multipart/form-data" が指定されているため、ファイルアップロードが可能になります。method="post"でデータをPHPに送信します。

  • ドラッグ&ドロップエリア (`<div id=”dropArea”>`):

    このdivが、ドラッグ&ドロップ操作を受け付けるメインの領域です。見た目を整え、ユーザーに操作を促すためのテキストを含みます。

  • ファイル選択ボタン (`<label for=”imageInput” class=”file-select-button”>`):

    通常のファイル入力フィールド<input type="file">は非表示(display: none;)にし、代わりに<label>要素を使ってカスタムボタンをデザインしています。for="imageInput"とすることで、<label>をクリックすると関連付けられた<input>がクリックされたのと同じ動作をします。

  • 非表示のファイル入力 (`<input type=”file” name=”image” id=”imageInput”>`):

    実際にファイルをPHPに送信するための隠された入力フィールドです。accept=".jpg, .jpeg, .png"属性により、ブラウザのファイル選択ダイアログで選択できるファイルタイプを制限していますが、これはあくまでクライアントサイドの補助的な機能であり、サーバーサイドでの検証(PHP部分)が重要です。

  • ファイル名表示 (`<div id=”fileNameDisplay”>`):

    JavaScriptによって、選択またはドロップされたファイルの名前が表示される領域です。

  • メッセージとダウンロードリンク:

    PHPから返された変換結果のメッセージやダウンロードリンクが表示される場所です。

CSS: 洗練されたUIデザイン

ユーザーが快適に操作できるよう、デザインを整えています。

  • レイアウト (`body`, `.container`):

    bodyにFlexboxを使い、コンテンツを画面中央に配置しています。.containerはフォーム全体を囲むボックスで、影や角丸を設定してモダンな見た目にしています。

  • ドラッグ&ドロップエリア (`.drop-area`):

    点線ボーダー、背景色、パディングなどで、ファイルがドロップできる領域であることを視覚的に示しています。.highlightクラスはJavaScriptで追加され、ドラッグオーバー中に領域がハイライトされる効果を提供します。

  • ファイル入力のカスタム化 (`input[type=”file”]`, `.file-select-button`):

    ブラウザ標準のファイル入力フィールドのスタイルは変更が難しい場合が多いため、display: none;で非表示にし、代わりに<label>要素を使ってデザイン性の高い「ファイルを選択」ボタンを作成しています。

  • メッセージのスタイル (`.message`, `.success`, `.error`):

    変換の成功・失敗に応じて、異なる色でメッセージを表示し、ユーザーに分かりやすくフィードバックします。

JavaScript: ドラッグ&ドロップ機能の実装

クライアントサイドでドラッグ&ドロップ操作を検知し、ファイルの情報を処理します。

  • イベントリスナーの登録:

    dragenter, dragover, dragleave, dropといったHTML5のドラッグ&ドロップAPIのイベントを監視しています。特に、document.bodyにもイベントリスナーを設定して、意図しない場所へのファイルのドロップを防いでいます。

  • `preventDefaults(e)`:

    ブラウザのデフォルトの動作(例: ファイルをドロップするとそのファイルがブラウザで開かれるなど)を抑制するために、イベントのデフォルト動作と伝播を停止しています。

  • `handleDrop(e)`:

    ファイルがドロップされた際に実行される主要な関数です。e.dataTransfer.filesからドロップされたファイルリストを取得します。この例では最初のファイル(files[0])のみを処理します。

    最も重要なのは、取得したFileオブジェクトを新しいDataTransferオブジェクトに格納し、それを非表示の<input type="file">要素の.filesプロパティに代入している点です。これにより、まるでユーザーが直接ファイル選択ダイアログでファイルを選んだかのように、フォームがファイルを受け取れる状態になります。このテクニックは、JavaScriptからサーバーにファイルを送信する際の基本的な手法です。

    また、選択されたファイルの名前をfileNameDisplayに表示し、ユーザーにどのファイルが選択されたかをフィードバックします。

  • ファイルタイプの検証:

    ドロップまたは選択されたファイルのMIMEタイプをチェックし、JPGまたはPNG以外の場合はアラートを表示して入力をクリアします。これはクライアントサイドでの簡易的なチェックであり、セキュリティ上、PHP側での検証が必須です。

PHP: サーバーサイドの画像変換ロジック

アップロードされた画像ファイルを処理し、WebPに変換して保存します。

  • ファイルアップロードの処理 (`$_FILES`):

    フォームが送信されると、PHPのスーパーグローバル変数$_FILESにアップロードされたファイルのメタデータ(名前、タイプ、一時パスなど)が格納されます。move_uploaded_file()関数を使って、一時保存されたファイルを目的のuploadsディレクトリに移動させます。

  • GDライブラリによる画像処理:

    • `imagecreatefromjpeg()` / `imagecreatefrompng()`:

      アップロードされたJPGまたはPNGファイルをPHPのGD画像リソースとして読み込みます。このリソースは、メモリ上で画像を操作するためのハンドルとして機能します。

    • PNGの透過情報保持:

      PNG画像の場合、透過情報を正しくWebPに変換するために、imagealphablending($sourceImage, false);imagesavealpha($sourceImage, true);を設定することが重要です。これにより、変換時にピクセルのアルファ値が保持されます。

    • `imagewebp()`:

      GD画像リソースをWebP形式で指定されたパスに保存する核心となる関数です。第三引数で品質(0~100)を指定できます。この値を調整することで、ファイルサイズと画質のバランスを最適化できます。

    • `imagedestroy()`:

      画像処理が完了したら、メモリを解放するためにimagedestroy()を呼び出すことが推奨されます。特に大きな画像を多数処理する場合に重要です。

  • ファイルタイプ検証とエラーハンドリング:

    アップロードされたファイルの拡張子をチェックし、JPGまたはPNG以外は拒否します。また、ファイルのアップロードや変換が失敗した場合に、ユーザーに分かりやすいメッセージを表示します。

  • ダウンロードリンクの生成:

    変換が成功すると、生成されたWebPファイルへのパスを含むダウンロードリンクを作成し、<a>タグのdownload属性を使って、クリック時にファイルをダウンロードさせるようにします。

さらなる改善とセキュリティ対策

このツールは基本的な機能を提供しますが、実運用を考えるともっと多くの改善点があります。

セキュリティの強化

  • ファイルサイズ制限:

    php.iniupload_max_filesizepost_max_sizeでアップロード可能なファイルの最大サイズを制限します。JavaScript側でも、アップロード前にファイルサイズをチェックしてユーザーにフィードバックすることが望ましいです。

  • MIMEタイプ検証の強化:

    PHP側で、$_FILES['image']['type']だけでなく、finfo_open()関数などを使って実際のMIMEタイプを厳密に検証する方が安全です。ファイル拡張子は偽装が容易なため、これだけでは不十分です。

  • ファイル名サニタイズ:

    アップロードされるファイル名に、ディレクトリトラバーサル攻撃などに利用される可能性のある特殊文字(../など)が含まれていないか、あるいは無害な名前に変更(例: ユニークIDをファイル名にする)するなど、サニタイズ処理を行うべきです。

  • ディレクトリ権限:

    前述の通り、uploadsディレクトリには最小限の権限を与え、実行権限は絶対に与えないでください。

  • 変換後の元ファイル削除:

    `unlink($uploadFile);`のコメントアウトを外すことで、変換後の元画像をサーバーから自動的に削除し、ディスクスペースを節約できます。ただし、元画像が必要な場合は削除しないでください。

機能拡張のアイデア

  • 複数ファイルのアップロード:

    <input type="file" multiple>を使用し、JavaScriptとPHPで複数のファイルを処理できるように拡張できます。

  • 非同期アップロード (Ajax):

    フォーム送信時にページ全体をリロードするのではなく、Ajaxを使ってバックグラウンドでファイルをアップロード・変換し、結果だけをUIに反映させることで、よりスムーズなユーザーエクスペリエンスを提供できます。

  • 品質設定のUI:

    WebP変換の品質($quality)をユーザーがUIから調整できるように、スライダーなどを追加することができます。

  • エラーログ:

    変換エラーなどの詳細な情報を、ユーザーには表示せず、サーバーサイドのログファイルに記録するようにすると、デバッグや問題の特定に役立ちます。

  • WebPのサイズ削減効果表示:

    変換前と変換後のファイルサイズを比較し、どれだけサイズが削減されたかをユーザーに提示すると、WebPのメリットを実感しやすくなります。

まとめ

この記事では、PHPのGDライブラリを活用して、JPG/PNG画像をWebPに変換し、さらにドラッグ&ドロップで手軽にアップロードできるWebP変換ツールを構築する方法を解説しました。

Webサイトのパフォーマンス向上は、現代のWeb開発において避けて通れない課題です。WebP導入はその強力な一手となるでしょう。今回紹介したツールをベースに、ぜひご自身のプロジェクトやニーズに合わせてカスタマイズしてみてください。

 
※参考にされる場合は自己責任でお願いします。