中級 PHP MySQL jQuery 画像アップロード

画像付きフォームをデータベースに保存・編集する

「行を追加」ボタンで増やせるテキスト+画像のフォームを作り、画像を3つのサイズに縮小して保存、データベースに登録します。表示画面と、画像の差し替え・削除ができる編集画面まで作る、少し本格的なサンプルです。

このサンプルは2017〜2018年ごろに作成したものです。PHPのバージョンが上がり、そのままでは動かなくなっていたため、2026年に PHP 8.3 で動くように書き直し、解説も新しくしました。

DEMO 実際に動かしてみよう

追加画面 | 表示画面 | 編集画面

テキスト画像(1MBまで)


※ このデモのデータは、見に来たすべての人で共有されます。最新20件を超えると古いものから自動で削除されます。個人情報や人に見られて困る画像は送らないでください。

解説コードを読んでみよう

このサンプルでわかること

  • 同じ名前の入力欄を何個でも増やせるフォームの作り方(name="text[]")
  • 複数の画像を受け取ったときの $_FILES の中身と、安全なチェックのしかた
  • PHP の GD 機能で、画像を中サイズ・サムネイルに縮小して保存する方法
  • PDO のプリペアドステートメントで、データベースに登録・更新・削除する方法
  • 編集画面で「既存の行」と「新しく追加した行」を見分けて処理する方法

上のデモは、追加画面 → 表示画面 → 編集画面の3つの画面でできています。まずは追加画面で画像を2〜3枚登録し、表示画面と編集画面も触ってみてから解説を読むと、コードの役割がつかみやすくなります。

使っているファイル

  • index.php追加画面。「行を追加」で入力欄を増やせるフォーム
  • upload.php追加画面から送られた内容をチェックし、画像を保存してデータベースに登録
  • view.php表示画面。登録された画像とテキストの一覧
  • edit.php編集画面。テキストの書き換え、画像の差し替え・削除、行の削除
  • update.php編集画面から送られた内容でデータベースを更新
  • del_img.php / del_line.php編集画面のボタンから ajax で呼ばれ、画像だけ/行ごと削除
  • del_all.php「全て削除する」ボタンの処理
  • common.php全ファイル共通の設定と関数(画像のチェック・保存・縮小など)
  • db.phpデータベースに接続する関数
  • img_full/ img_med/ img_thu/画像の保存先(原寸・中サイズ・サムネイル)

処理の流れ

画面・処理やること次に進む先
index.php(画面)テキストと画像を入力して送信upload.php
upload.php(処理)チェック → 画像を保存・縮小 → INSERTview.php へ自動で移動
view.php(画面)SELECT して一覧表示—
edit.php(画面)SELECT した内容をフォームに入れて表示。画像・行の削除は ajaxupdate.php
update.php(処理)既存の行は UPDATE、新しい行は INSERTview.php へ自動で移動

「処理」のファイルは画面を持たず、仕事が終わったら header('Location: ...') で次の画面へ移動させます。こうしておくと、表示画面で再読み込み(F5)をしても、同じデータが二重に登録されません。

1. データベースのテーブルを用意する

1行が「テキスト1つ+画像1枚」です。画像そのものはファイルとしてフォルダに保存し、データベースにはファイル名だけを記録します。

テーブルの作成 SQL
CREATE TABLE sp_img_01 (
  ID      INT AUTO_INCREMENT PRIMARY KEY,  -- 行の番号(自動で増える)
  user_id INT NOT NULL,                    -- 誰のデータか(サンプルでは 1 固定)
  post_id INT NOT NULL,                    -- どの記事のデータか(サンプルでは 1 固定)
  text    VARCHAR(255) NOT NULL DEFAULT '',
  img     VARCHAR(255) NOT NULL DEFAULT '', -- 画像のファイル名(画像なしなら空)
  date    DATETIME DEFAULT CURRENT_TIMESTAMP -- 登録日時(画像の保存フォルダにも使う)
) DEFAULT CHARSET=utf8mb4;

2. 行を増やせるフォームを作る(index.php)

ポイントは入力欄の名前です。name="text[]" のように最後に [] を付けると、同じ名前の入力欄がいくつあっても、PHP 側では配列としてまとめて受け取れます。

index.php(13〜35行目) PHP
<form action="upload.php" method="post" enctype="multipart/form-data">
  <input type="hidden" name="MAX_FILE_SIZE" value="1048576">
  <table class="rows">
    <thead>
      <tr><th>テキスト</th><th>画像(1MBまで)</th></tr>
    </thead>
    <tbody>
      <tr>
        <td><input type="text" name="text[]" maxlength="100"></td>
        <td>
          <div class="preview"></div>
          <input type="file" name="img[]" accept="image/*">
        </td>
      </tr>
    </tbody>
  </table>
  <p>
    <button type="button" id="add">+ 行を追加</button>
    <span id="message" class="msg"></span>
  </p>
  <hr>
  <input type="submit" value="送信して保存する">
</form>
  • enctype="multipart/form-data" … ファイルを送るフォームには必ず付けます。これがないと画像が届きません。
  • MAX_FILE_SIZE … ファイルサイズの上限(バイト)。超えると PHP がエラー番号 2 を返します(ただし最終的なチェックは PHP 側で行います)。
  • accept="image/*" … ファイル選択の画面で画像だけを表示するためのヒントです。

「行を追加」ボタンを押すと、jQuery で同じ形の行を表の最後に足します。画像を選んだときは FileReader で、送信前にプレビューを表示しています。

index.php(47〜76行目) JavaScript
<script>
$(function () {
    var MAX_ROWS = 5;  // 一度に送信できる行数

    // 画像を選んだら、送信前にプレビューを表示する
    $(document).on('change', 'input[type=file]', function () {
        var $preview = $(this).siblings('.preview');
        $preview.empty();
        if (!this.files[0]) return;
        var reader = new FileReader();
        reader.onload = function (e) {
            $preview.html('<img src="' + e.target.result + '" alt="">');
        };
        reader.readAsDataURL(this.files[0]);
    });

    // 「行を追加」ボタン
    $('#add').on('click', function () {
        if ($('.rows tbody tr').length >= MAX_ROWS) {
            $('#message').text('※ 一度に追加できるのは' + MAX_ROWS + '行までです。');
            return;
        }
        var row = '<tr>' +
            '<td><input type="text" name="text[]" maxlength="100"></td>' +
            '<td><div class="preview"></div><input type="file" name="img[]" accept="image/*"></td>' +
            '</tr>';
        $('.rows tbody').append(row);
    });
});
</script>

あとから追加した行にもプレビューが効くように、$('input[type=file]').on(...) ではなく $(document).on('change', 'input[type=file]', ...) と書いています。これを「イベントの委譲」といい、後から作られた要素にもイベントが届く書き方です。

3. 送られてきたデータの形を知る($_FILES)

2行分(1行目は画像あり、2行目は画像なし)を送信したとき、PHP には次のような形で届きます。$_FILES['img'] は「名前の一覧」「エラー番号の一覧」…のように、項目ごとに配列になっているのが特徴です。

送信内容の例 TEXT
// print_r($_POST);
Array
(
    [MAX_FILE_SIZE] => 1048576
    [text] => Array ( [0] => 夏の空  [1] => 画像なしの行 )
)

// print_r($_FILES);
Array
(
    [img] => Array
        (
            [name]     => Array ( [0] => sky.jpg     [1] => )
            [type]     => Array ( [0] => image/jpeg  [1] => )
            [tmp_name] => Array ( [0] => /tmp/phpA1b2C3  [1] => )
            [error]    => Array ( [0] => 0           [1] => 4 )
            [size]     => Array ( [0] => 183205      [1] => 0 )
        )
)

つまり「$i 番目の行」の画像は、$_FILES['img']['tmp_name'][$i]、そのエラー番号は $_FILES['img']['error'][$i] のように取り出します。テキストの $_POST['text'][$i] と番号がそろっているので、同じ $i で1行分をまとめて扱えます。

4. 画像をチェックする(common.php)

受け取ったファイルが本当に画像か、サイズは大丈夫かを確認します。エラー番号 4(ファイルが選ばれていない)は「テキストだけの行」なので、エラーにはしません。

common.php(28〜59行目) PHP
/**
 * 送信された画像 1 枚をチェックする
 * 問題がなければ null、問題があればエラーメッセージを返す
 */
function check_image(array $files, int $i): ?string
{
    $error = $files['error'][$i] ?? UPLOAD_ERR_NO_FILE;
    $name  = $files['name'][$i] ?? '';

    switch ($error) {
        case UPLOAD_ERR_OK:          // 0: 正常
            break;
        case UPLOAD_ERR_NO_FILE:     // 4: 画像が選ばれていない行(テキストだけの行はOK)
            return null;
        case UPLOAD_ERR_INI_SIZE:    // 1: php.ini の上限を超えた
        case UPLOAD_ERR_FORM_SIZE:   // 2: フォームの MAX_FILE_SIZE を超えた
            return "{$name} のファイルサイズが大きすぎます(1MBまで)。";
        default:
            return "{$name} のアップロードに失敗しました(エラー番号 {$error})。";
    }

    if ($files['size'][$i] > MAX_SIZE) {
        return "{$name} のファイルサイズが大きすぎます(1MBまで)。";
    }

    // 拡張子ではなく、ファイルの中身から画像の種類を調べる
    $info = @getimagesize($files['tmp_name'][$i]);
    if ($info === false || image_ext($info['mime']) === null) {
        return "{$name} は画像ファイル(GIF・JPEG・PNG)ではありません。";
    }
    return null;
}
エラー番号定数意味
0UPLOAD_ERR_OK正常に届いた
1UPLOAD_ERR_INI_SIZEサーバー(php.ini)の上限を超えた
2UPLOAD_ERR_FORM_SIZEフォームの MAX_FILE_SIZE を超えた
3UPLOAD_ERR_PARTIAL途中までしか届かなかった
4UPLOAD_ERR_NO_FILEファイルが選ばれていない

ファイル名の拡張子(.jpg など)は、送る人が自由に変えられます。そのため、getimagesize() でファイルの中身から画像の種類(MIMEタイプ)を調べ、GIF・JPEG・PNG のときだけ受け付けています。

upload.php では、まずすべての行をチェックしてから保存を始めます。途中の1枚がエラーのときに、前の数枚だけ保存されてしまうのを防ぐためです。

upload.php(17〜36行目) PHP
$texts = $_POST['text'] ?? [];   // name="text[]" → 配列で届く
$files = $_FILES['img'] ?? [];   // name="img[]"  → 配列で届く

// ---- 1. チェック ---------------------------------------------
if (!is_array($texts) || count($texts) === 0) {
    show_error_page(['送信内容がありません。'], 'index.php');
}
if (count($texts) > MAX_ROWS) {
    show_error_page(['一度に送信できるのは' . MAX_ROWS . '行までです。'], 'index.php');
}
$errors = [];
foreach (array_keys($texts) as $i) {
    $error = check_image($files, $i);
    if ($error !== null) {
        $errors[] = $error;
    }
}
if ($errors) {
    show_error_page($errors, 'index.php');  // 1枚でも問題があれば何も保存しない
}

5. 画像を保存する(ファイル名と日付フォルダ)

保存するときは、送られてきたファイル名を使わず、sha1(random_bytes(16)) で作ったランダムな40文字を名前にします。同じ名前のファイルが上書きされたり、日本語や記号の入ったファイル名でトラブルが起きたりするのを防げます。

common.php(74〜98行目) PHP
/**
 * アップロードされた画像を保存し、中サイズとサムネイルも作る
 * @return string 保存したファイル名(ランダムな40文字+拡張子)
 */
function save_image(string $tmp, string $now): string
{
    $info = getimagesize($tmp);
    $name = sha1(random_bytes(16)) . image_ext($info['mime']);  // 他と重ならない名前
    $dir  = date_dir($now);

    // 日付ごとのフォルダを作る(例: img_full/2026/10/27)
    foreach (['img_full', 'img_med', 'img_thu'] as $d) {
        if (!is_dir(__DIR__ . "/{$d}/{$dir}")) {
            mkdir(__DIR__ . "/{$d}/{$dir}", 0755, true);
        }
    }

    $full = __DIR__ . "/img_full/{$dir}/f{$name}";
    if (!move_uploaded_file($tmp, $full)) {
        throw new RuntimeException('画像を保存できませんでした。');
    }
    resize_image($full, $info, MIDDLE_WIDTH,    __DIR__ . "/img_med/{$dir}/m{$name}");
    resize_image($full, $info, THUMBNAIL_WIDTH, __DIR__ . "/img_thu/{$dir}/t{$name}");
    return $name;
}
  • 保存先は img_full/2026/10/27/ のような日付ごとのフォルダです。1つのフォルダにファイルが増えすぎるのを防ぎます。mkdir() の3つ目の引数 true で、途中のフォルダもまとめて作れます。
  • アップロードされたファイルは、いったんサーバーの一時フォルダに置かれます。move_uploaded_file() で正式な保存先へ移動します。
  • サイズごとに先頭の1文字を変えています(原寸 f、中 m、サムネイル t)。

6. GD で画像を縮小する

一覧画面で原寸の大きな画像を並べると表示が重くなるので、横幅200pxの中サイズと100pxのサムネイルを作っておきます。

common.php(100〜125行目) PHP
/** 画像を指定の横幅に縮小して保存する(GD を使用) */
function resize_image(string $src, array $info, int $width, string $dest): void
{
    [$w, $h] = $info;  // 元の幅と高さ
    if ($w <= $width) {
        copy($src, $dest);  // もともと小さい画像はそのままコピー
        return;
    }
    $height = (int)round($h * $width / $w);  // 縦横の比率を保った高さ

    $srcImage = match ($info['mime']) {
        'image/gif'  => imagecreatefromgif($src),
        'image/jpeg' => imagecreatefromjpeg($src),
        'image/png'  => imagecreatefrompng($src),
    };
    $newImage = imagecreatetruecolor($width, $height);  // 空の画像を作る
    imagealphablending($newImage, false);               // PNG の透明部分を保つ
    imagesavealpha($newImage, true);
    imagecopyresampled($newImage, $srcImage, 0, 0, 0, 0, $width, $height, $w, $h);

    match ($info['mime']) {
        'image/gif'  => imagegif($newImage, $dest),
        'image/jpeg' => imagejpeg($newImage, $dest, 85),
        'image/png'  => imagepng($newImage, $dest),
    };
}
  1. 縦横の比率が変わらないように、新しい高さを 元の高さ × 新しい幅 ÷ 元の幅 で計算します。
  2. imagecreatefromjpeg() などで元の画像を読み込み、imagecreatetruecolor() で新しいサイズの空の画像を作ります。
  3. imagecopyresampled() で、元の画像を縮めながら空の画像に描き写します。
  4. imagejpeg() などで、ファイルとして保存します(JPEG の 85 は画質)。

match は PHP 8 から使える書き方で、switch を短く書けます。画像の種類ごとに使う関数を切り替えています。

7. データベースに登録する(PDO のプリペアドステートメント)

upload.php(38〜74行目) PHP
// ---- 2・3. 保存と登録 ----------------------------------------
$dbh = db_connect();
$now = date('Y-m-d H:i:s');   // 画像フォルダの日付と、データベースの日付を同じにする

try {
    $sql  = 'INSERT INTO ' . TABLE . ' (user_id, post_id, text, img, date)
             VALUES (:user_id, :post_id, :text, :img, :date)';
    $stmt = $dbh->prepare($sql);

    foreach ($texts as $i => $text) {
        $text = mb_substr(trim((string)$text), 0, 100);
        $img  = '';
        if (($files['error'][$i] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK) {
            $img = save_image($files['tmp_name'][$i], $now);
        }
        if ($text === '' && $img === '') {
            continue;  // 何も入力されていない行は登録しない
        }
        $stmt->execute([
            ':user_id' => USER_ID,
            ':post_id' => POST_ID,
            ':text'    => $text,
            ':img'     => $img,
            ':date'    => $now,
        ]);
    }

    // 公開デモなので、古いデータは自動で削除(最新 DEMO_MAX_ROWS 件だけ残す)
    demo_prune_images($dbh, TABLE, __DIR__);

} catch (Throwable $e) {
    show_error_page(['保存中にエラーが発生しました。時間をおいて、もう一度お試しください。'], 'index.php');
}

// ---- 4. 表示画面へ -------------------------------------------
header('Location: view.php');
exit;

SQL の中の :text や :img は「あとで値を入れる場所(プレースホルダ)」です。prepare() で SQL を準備し、execute() に値を渡します。値は PDO が安全な形に変換してくれるので、入力に ' などの記号が含まれていても SQL が壊れません(SQLインジェクション対策)。

$now を1回だけ作り、画像を置くフォルダの日付とデータベースの date の両方に使っています。画像の場所はあとで date から組み立てるので、2つが必ず一致するようにしているのです。

8. 一覧を表示する(view.php)

データベースから取り出した行を foreach で並べます。画像の URL は common.php の image_url() で、日付とファイル名から組み立てます。

view.php(23〜32行目) PHP
<div class="gallery">
<?php foreach ($data as $row): ?>
  <figure>
<?php if ($row['img'] !== ''): ?>
    <a href="<?= h(image_url($row, 'full')) ?>" target="_blank"><img src="<?= h(image_url($row, 'med')) ?>" alt=""></a>
<?php else: ?>
    <div class="noimg">画像なし</div>
<?php endif; ?>
    <figcaption><?= h($row['text']) ?></figcaption>
  </figure>

データベースの中身を画面に出すときは、必ず h()(htmlspecialchars())を通します。入力に <script> などが含まれていても、ただの文字として表示されます(XSS対策)。

9. 編集画面と、既存の行・新しい行の見分け方

編集画面では、各行に name="ID[]" の隠し項目で行の ID を入れておきます。「行を追加」で増やした行は ID を空にしておきます。

edit.php(25〜40行目) PHP
<?php foreach ($data as $row): ?>
      <tr data-id="<?= h($row['ID']) ?>">
        <td>
          <input type="text" name="text[]" value="<?= h($row['text']) ?>" maxlength="100">
          <input type="hidden" name="ID[]" value="<?= h($row['ID']) ?>">
        </td>
        <td>
          <div class="preview"></div>
<?php if ($row['img'] !== ''): ?>
          <div class="current">
            <a href="<?= h(image_url($row, 'full')) ?>" target="_blank"><img src="<?= h(image_url($row, 'med')) ?>" alt=""></a>
            <button type="button" class="del-img">画像を削除</button>
          </div>
<?php endif; ?>
          <input type="file" name="img[]" accept="image/*"<?= $row['img'] !== '' ? ' style="display:none"' : '' ?>>
        </td>

画像がある行では、ファイル選択欄を display:none で隠しているだけで、消してはいません。どの行にも text・ID・img が1つずつあるので、送信したときに3つの配列の番号($i)がずれないのです。

update.php では、ID があれば UPDATE、空なら INSERT、と分けて処理します。

update.php(37〜76行目) PHP
try {
    foreach ($texts as $i => $text) {
        $text    = mb_substr(trim((string)$text), 0, 100);
        $id      = (int)($ids[$i] ?? 0);
        $hasFile = ($files['error'][$i] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK;

        if ($id > 0) {
            // ===== 既存の行 =====
            $row = find_row($dbh, $id);
            if ($row === null) {
                continue;  // 別の人が先に削除していた
            }
            $stmt = $dbh->prepare('UPDATE ' . TABLE . ' SET text = :text WHERE ID = :id');
            $stmt->execute([':text' => $text, ':id' => $id]);

            if ($hasFile) {
                // 新しい画像を保存し、古い画像ファイルは消す
                $img = save_image($files['tmp_name'][$i], $now);
                delete_image_files($row);
                // 画像の置き場所は日付で決まるので、date も今日の日付に更新する
                $stmt = $dbh->prepare('UPDATE ' . TABLE . ' SET img = :img, date = :date WHERE ID = :id');
                $stmt->execute([':img' => $img, ':date' => $now, ':id' => $id]);
            }
        } else {
            // ===== 新しく追加した行 =====
            $img = $hasFile ? save_image($files['tmp_name'][$i], $now) : '';
            if ($text === '' && $img === '') {
                continue;
            }
            $stmt = $dbh->prepare('INSERT INTO ' . TABLE . ' (user_id, post_id, text, img, date)
                                   VALUES (:user_id, :post_id, :text, :img, :date)');
            $stmt->execute([
                ':user_id' => USER_ID,
                ':post_id' => POST_ID,
                ':text'    => $text,
                ':img'     => $img,
                ':date'    => $now,
            ]);
        }
    }

画像を差し替えたときは、古い画像ファイルを削除し、date も今日の日付に更新しています。画像の保存フォルダは日付で決まるので、こうしないと新しい画像の場所がわからなくなります。

10. ajax で画像・行を削除する

「画像を削除」「行を削除」ボタンは、ページを移動せずに $.post() で削除用の PHP を呼びます。送るのは行の ID だけです。

edit.php(83〜101行目) JavaScript
    // 画像を削除(ajax)。送るのは行の ID だけ
    $(document).on('click', '.del-img', function () {
        var $tr = $(this).closest('tr');
        $.post('del_img.php', { ID: $tr.data('id') }).done(function () {
            $tr.find('.current').remove();              // 表示中の画像を消す
            $tr.find('input[type=file]').show();         // 新しい画像を選べるようにする
        });
    });

    // 行を削除(ajax)
    $(document).on('click', '.del-line', function () {
        var $tr = $(this).closest('tr');
        if (!$tr.data('id')) {          // まだ保存していない行は、画面から消すだけ
            $tr.remove(); added--; return;
        }
        $.post('del_line.php', { ID: $tr.data('id') }).done(function () {
            $tr.remove();
        });
    });
del_img.php(8〜24行目) PHP
header('Content-Type: application/json; charset=utf-8');

$id = (int)($_POST['ID'] ?? 0);
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || $id <= 0) {
    http_response_code(400);
    echo json_encode(['ok' => false]);
    exit;
}

$dbh = db_connect();
$row = find_row($dbh, $id);
if ($row !== null) {
    delete_image_files($row);  // 画像ファイル(3サイズ)を削除
    $stmt = $dbh->prepare('UPDATE ' . TABLE . " SET img = '' WHERE ID = :id");
    $stmt->execute([':id' => $id]);
}
echo json_encode(['ok' => true]);

以前のバージョンでは、ブラウザから「年/月/日/ファイル名」を送り、そのまま unlink() していました。これだと ../ を混ぜた値を送られると、サーバー上の別のファイルまで消せてしまいます。今のバージョンでは ID だけを受け取り、消すファイルの場所はサーバー側でデータベースから調べるので、外から場所を指定できません。

「全て削除する」は、リンクではなく POST で送るフォームにしています。GET のリンクだと、検索エンジンのロボットがリンクをたどっただけでデータが消えてしまうからです。

del_all.php(8〜22行目) PHP
// 削除のような「データを変える処理」は POST のときだけ行う
// (GET のリンクだと、検索エンジンのロボットが巡回しただけで消えてしまうため)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $dbh = db_connect();
    foreach (all_rows($dbh) as $row) {
        delete_image_files($row);
    }
    $stmt = $dbh->prepare('DELETE FROM ' . TABLE . ' WHERE user_id = :user_id AND post_id = :post_id');
    $stmt->execute([':user_id' => USER_ID, ':post_id' => POST_ID]);
}

// 戻り先は決まった2つのどちらかだけ(外部のURLに飛ばされないように)
$back = ($_POST['back'] ?? '') === 'edit' ? 'edit.php' : 'index.php';
header('Location: ' . $back);
exit;

PHP 8.3 で書き直したポイント

  • 定数名のタイプミスで止まっていた … 旧版には MIDEEL_WIDTH(正しくは MIDDLE_WIDTH)という誤字がありました。PHP 7 までは警告だけで動きましたが、PHP 8 では存在しない定数がエラーになり、画像のアップロードがすべて失敗していました。
  • PDO が例外を投げるようになった … PHP 8 から PDO の初期設定が「エラー時は例外」に変わりました。旧版の更新処理には「画像のない既存の行を、毎回新しい行として INSERT しようとする」不具合があり、以前は黙って失敗していたものが、エラーで止まるようになっていました。ID の有無で UPDATE と INSERT を分けるように直しています。
  • count() の扱いが厳しくなった … 配列でない値を count() に渡すとエラーになるため、$_POST['text'] ?? [] のように必ず配列にしてから数えています。
  • 小さい画像の中サイズが作られていなかった … 元の画像が200pxより小さいと中サイズが作られず、一覧で画像が表示されませんでした。小さい画像はそのままコピーするようにしています。
  • 削除処理の安全対策 … 上で説明したとおり、ファイルの場所をブラウザから受け取らないようにしました。

common.php の MIDDLE_WIDTH を 300 に変えたり、imagejpeg() の画質 85 を 30 にしたりして、保存される画像がどう変わるか確かめてみましょう。

↑