259
265

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

画像アップロード処理サンプル集

Last updated at Posted at 2013-07-17

【2021/10/15 追記】
この記事は更新が停止されています。現在では筆者の思想が変化している面もありますので,過去の記事として参考程度にご覧ください。

前書き

より一般化したものについては 「ファイルアップロードの例外処理はこれぐらいしないと気が済まない」 を参照。ここではそれを元に画像ファイルに限定して、いくつかのパターンで例を構成してみる。また、フォームの送信と受信を同一ファイルで行うとする。

サンプル集

1. exif_imagetype 関数を用いてチェックを行う

これは、画像に関するMIMEタイプを整数として返す関数である。finfoクラスが使えない環境においても統一的にこちらの関数は使えるはず。但し、画像の形式を判別するために必要なだけのバイト数を読み込めない場合にエラーを発生するので、 エラー制御演算子 @ を用いて抑制する必要がある。以前ここでは getimagesize 関数を用いる手法を掲載していたが、こちらもマニュアルには記載されていないだけでエラーを発生するリスクを抱えていたので、抑制がいずれ必要となるならば…ということで一番動作が速いこの関数を採用することにした。

ss (2014-04-05 at 08.36.35).png

なお、この定数から実際の文字列形式の MIMEタイプ拡張子 を得るための関数も用意されている。

<?php

if (isset($_FILES['upfile']['error']) && is_int($_FILES['upfile']['error'])) {
    
    try {
        
        // $_FILES['upfile']['error'] の値を確認
        switch ($_FILES['upfile']['error']) {
            case UPLOAD_ERR_OK: // OK
                break;
            case UPLOAD_ERR_NO_FILE:   // ファイル未選択
                throw new RuntimeException('ファイルが選択されていません');
            case UPLOAD_ERR_INI_SIZE:  // php.ini定義の最大サイズ超過
            case UPLOAD_ERR_FORM_SIZE: // フォーム定義の最大サイズ超過
                throw new RuntimeException('ファイルサイズが大きすぎます');
            default:
                throw new RuntimeException('その他のエラーが発生しました');
        }
        
        // $_FILES['upfile']['mime']の値はブラウザ側で偽装可能なので、MIMEタイプを自前でチェックする
        $type = @exif_imagetype($_FILES['upfile']['tmp_name']);
        if (!in_array($type, [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG], true)) {
            throw new RuntimeException('画像形式が未対応です');
        }
        
        // ファイルデータからSHA-1ハッシュを取ってファイル名を決定し、ファイルを保存する
        $path = sprintf('./uploads/%s%s', sha1_file($_FILES['upfile']['tmp_name']), image_type_to_extension($type));
        if (!move_uploaded_file($_FILES['upfile']['tmp_name'], $path)) {
            throw new RuntimeException('ファイル保存時にエラーが発生しました');
        }
        chmod($path, 0644);
        
        $msg = ['green', 'ファイルは正常にアップロードされました'];
        
    } catch (RuntimeException $e) {
        
        $msg = ['red', $e->getMessage()];
        
    }
    
}

// XHTMLとしてブラウザに認識させる
// (IE8以下はサポート対象外w)
header('Content-Type: application/xhtml+xml; charset=utf-8');

?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>画像アップロード</title>
</head>
<body>
<?php if (isset($msg)): ?>
  <fieldset>
    <legend>結果</legend>
    <span style="color:<?=$msg[0]?>;"><?=$msg[1]?></span>
  </fieldset>
<?php endif; ?>
  <form enctype="multipart/form-data" method="post" action="">
    <fieldset>
      <legend>画像ファイルを選択(GIF, JPEG, PNGのみ対応)</legend>
      <input type="file" name="upfile" /><br />
      <input type="submit" value="送信" />
    </fieldset>
  </form>
</body>
</html>

もしファイルではなく $data に文字列として保持しているデータをチェックしたい場合、以下のようにする。

@exif_imagetype(
    'data://application/octet-stream;base64,' .
    base64_encode($data)
)

2. imagecreatefromstring 関数から全てPNGとして保存する

環境によって差があるが、PHPのGDライブラリは GIF JPEG PNG に加えて BMP XPM に対応している場合もある。ここではこれら全てを自動判別できる imagecreatefromstring 関数を利用する。この関数はリソース生成失敗時にエラーを発生するため、1番目の例と同様に エラー制御演算子 @ を用いて抑制する必要がある。

<?php

if (isset($_FILES['upfile']['error']) && is_int($_FILES['upfile']['error'])) {
    
    try {
        
        // $_FILES['upfile']['error'] の値を確認
        switch ($_FILES['upfile']['error']) {
            case UPLOAD_ERR_OK: // OK
                break;
            case UPLOAD_ERR_NO_FILE:   // ファイル未選択
                throw new RuntimeException('ファイルが選択されていません');
            case UPLOAD_ERR_INI_SIZE:  // php.ini定義の最大サイズ超過
            case UPLOAD_ERR_FORM_SIZE: // フォーム定義の最大サイズ超過
                throw new RuntimeException('ファイルサイズが大きすぎます');
            default:
                throw new RuntimeException('その他のエラーが発生しました');
        }
        
        // GD画像リソースの生成を試みる
        if (!$img = @imagecreatefromstring(file_get_contents($_FILES['upfile']['tmp_name']))) {
            throw new RuntimeException('有効な画像ファイルを指定してください');
        }
        
        // ファイルデータからSHA-1ハッシュを取ってファイル名を決定し、保存する
        if (!imagepng($img, sprintf('./uploads/%s.png', sha1_file($_FILES['upfile']['tmp_name'])))) {
            throw new RuntimeException('ファイル保存時にエラーが発生しました');
        }
        
        $msg = ['green', 'ファイルは正常にアップロードされました'];
        
    } catch (RuntimeException $e) {
        
        $msg = ['red', $e->getMessage()];
        
    }

    // リソースを解放
    if (isset($img) && is_resource($img)) {
        imagedestroy($img);
    }
    
}

// XHTMLとしてブラウザに認識させる
// (IE8以下はサポート対象外w)
header('Content-Type: application/xhtml+xml; charset=utf-8');

?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>画像アップロード</title>
</head>
<body>
<?php if (isset($msg)): ?>
  <fieldset>
    <legend>結果</legend>
    <span style="color:<?=$msg[0]?>;"><?=$msg[1]?></span>
  </fieldset>
<?php endif; ?>
  <form enctype="multipart/form-data" method="post" action="">
    <fieldset>
      <legend>画像ファイルを選択</legend>
      <input type="file" name="upfile" /><br />
      <input type="submit" value="送信" />
    </fieldset>
  </form>
</body>
</html>

3. 複数の画像ファイルを受け取ってそれぞれリサイズして保存する

<input type="file" name="upfile[]">
<input type="file" name="upfile[]">

のように受け取った時、例えば各項目の error

$_FILES['upfile'][0]['error']
$_FILES['upfile'][1]['error']

になると思われがちだが、実際には

$_FILES['upfile']['error'][0]
$_FILES['upfile']['error'][1]

となることに要注意。なお、ここでは実際にサイズ取得が必要なので getimagesize 関数を利用することにする。

<?php

if (isset($_FILES['upfile']['error']) && is_array($_FILES['upfile']['error'])) {
    
    // 各ファイルをチェック
    foreach ($_FILES['upfile']['error'] as $k => $error) {
        
        try {
            
            // 更に配列がネストしていれば不正とする
            if (!is_int($error)) {
                throw new RuntimeException("[{$k}] パラメータが不正です");
            }
            
            // $_FILES['upfile']['error'][$k] の値を確認
            switch ($error) {
                case UPLOAD_ERR_OK: // OK
                    break;
                case UPLOAD_ERR_NO_FILE:   // ファイル未選択
                    continue 2;
                case UPLOAD_ERR_INI_SIZE:  // php.ini定義の最大サイズ超過
                case UPLOAD_ERR_FORM_SIZE: // フォーム定義の最大サイズ超過
                    throw new RuntimeException("[{$k}] ファイルサイズが大きすぎます");
                default:
                    throw new RuntimeException("[{$k}] その他のエラーが発生しました");
            }
            
            // $_FILES['upfile']['mime']の値はブラウザ側で偽装可能なので
            // MIMEタイプを自前でチェックする
            if (!$info = @getimagesize($_FILES['upfile']['tmp_name'][$k])) {
                throw new RuntimeException("[{$k}] 有効な画像ファイルを指定してください");
            }
            if (!in_array($info[2], [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG], true)) {
                throw new RuntimeException("[{$k}] 未対応の画像形式です");
            }
            
            // 画像処理に使う関数名を決定する
            $create = str_replace('/', 'createfrom', $info['mime']);
            $output = str_replace('/', '', $info['mime']);
            
            // 縦横比を維持したまま 120 * 120 以下に収まるサイズを求める
            if ($info[0] >= $info[1]) {
                $dst_w = 120;
                $dst_h = ceil(120 * $info[1] / max($info[0], 1));
            } else {
                $dst_w = ceil(120 * $info[0] / max($info[1], 1));
                $dst_h = 120;
            }
            
            // 元画像リソースを生成する
            if (!$src = @$create($_FILES['upfile']['tmp_name'][$k])) {
                throw new RuntimeException("[{$k}] 画像リソースの生成に失敗しました");
            }
            
            // リサンプリング先画像リソースを生成する
            $dst = imagecreatetruecolor($dst_w, $dst_h);
            
            // getimagesize関数で得られた情報も利用してリサンプリングを行う
            imagecopyresampled($dst, $src, 0, 0, 0, 0, $dst_w, $dst_h, $info[0], $info[1]);
            
            // ファイルデータからSHA-1ハッシュを取ってファイル名を決定し、保存する
            if (!$output(
                $dst,
                sprintf('./resized/%s%s',
                    sha1_file($_FILES['upfile']['tmp_name'][$k]),
                    image_type_to_extension($info[2])
                )
            )) {
                throw new RuntimeException("[{$k}] ファイル保存時にエラーが発生しました");
            }
            
            $msgs[] = ['green', "[{$k}] リサイズして保存しました"];
            
        } catch (RuntimeException $e) {
            
            $msgs[] = ['red', $e->getMessage()];
            
        }

        // リソースを解放
        if (isset($msg) && is_resource($img)) {
            imagedestroy($img);
        }
        if (isset($dst) && is_resource($dst)) {
            imagedestroy($dst);
        }
        
    }
    
}

// XHTMLとしてブラウザに認識させる
// (IE8以下はサポート対象外w)
header('Content-Type: application/xhtml+xml; charset=utf-8');

?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>画像アップロード</title>
</head>
<body>
<?php if (!empty($msgs)): ?>
  <ul>
<?php foreach ($msgs as $msg): ?>
    <li style="color:<?=$msg[0]?>;"><?=
    htmlspecialchars($msg[1], ENT_QUOTES, 'UTF-8')
    ?></li>
<?php endforeach; ?>
  </ul>
<?php endif; ?>
  <form enctype="multipart/form-data" method="post" action="">
    <fieldset>
      <legend>画像ファイルを選択(GIF, JPEG, PNGのみ対応)</legend>
      <ul>
<?php for ($i = 0; $i < 10; $i++): ?>
        <li><input type="file" name="upfile[]" /></li>
<?php endfor; ?>
      </ul>
      <input type="submit" value="送信" />
    </fieldset>
  </form>
</body>
</html>
259
265
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
259
265

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?