php – 画像ファイルのアップロードの備忘ソース

phpでhtmlフォームから画像を選択して、サーバにアップロードするソースを書きました。ほぼほぼググったら出てくるようなものばかりのコピペ品なのですが、自分のやりたいことをいくつか混ぜていったら、結構な数のWeb記事を調べていたので、自分用にまとめます。

 

自分のやりたかったこと点をあげます

・htmlのフォームは1つの選択ボタンで、複数の画像ファイルをアップできる
・アップできるファイル数に上限をつける
・画像以外のフォームもPOSTする
・横幅に制限をつけて、超えている場合は縦横比を維持してリサイズする
・exif情報を削除する
・もろもろのチェックを行ったうえで保存

——-4/25追記——-
・iphoneから画像アップした時に、勝手に横になったりする現象を回避
(後で、iphoneから試したら事象が発生したので追記)

上記を盛り込みました

 

入力フォームのphpソース

入力フォームのphpと受け取り側のphpは別にしました!まずは入力側のソースです。


<form action="./image_upload_received" name="form1" method="POST" enctype="multipart/form-data">
    <textarea name="text" cols="40" rows="2"></textarea>
    <input type="hidden" name="MAX_FILE_SIZE" value="3000000">
    <input type="file" id="select_file" accept="image/jpeg, image/gif, image/png" name="upfile[]" multiple="multiple">
    <a href="javascript:void(0)" onclick="input_check()">アップロードする</a>
</form>

htmlのフォームは1つの選択ボタンで、複数の画像ファイルをアップできる

input type=”file”のファイル選択用のタグにmultiple=”multiple”を指定するとファイルが複数選択できるようになります。

画像以外のフォームもPOSTする

multipart/form-dataを指定してても、他の通常POSTされるデータは特に問題ないようです、textareaはとりあえず一緒に送れることを確認するのにつけました。画像の説明とかをまとめて一緒に受け取りたい場合などですね。

formタグにenctypeの指定

ファイルをアップロードする際には必ずformにenctype=”multipart/form-data”を指定します。ファイルのデータはバウンダリ文字列というので複数に区切られて送信されるようですが、詳しく知りたい方はググってどうぞ~。

画像の指定

acceptで3種類に指定しています。androidで確認した時に、multipleがうまく動作しなくて1枚しか選べなかったのと、デフォルトの選択も画像ではなく使いづらかったです。(androidによっては?これでもうまく動かないこともあるかもしれません)

——-5/1追記——-
やっぱりうまくいかないパターンもありました・・・長くなったので別記事を書きました、そちらの苦肉の策を見て頂ければと思います。

function input_check(){

    //ファイル数が6コ以上の場合はNG
    var file_num_flg = 0;
    var fileList = document.getElementById('select_file').files;
    if(fileList.length > 6){
        file_num_flg = 1;
    }
    
    //ファイルサイズが3MBよりデカい場合はNG
    var file_size_flg = 0;
    for(i=0; i<fileList.length; i++){ if(fileList[i].size > 3000000){
            file_size_flg = 1;
        }
    }
    
    if(file_num_flg > 0){
        window.alert('添付ファイル数が上限を超えています');
    }else if(file_size_flg > 0){
        window.alert('投稿する画像のファイルサイズは3MB以下にしてください');
    }else{
        document.form1.submit();
    }
}

アップできるファイル数に上限をつける

こちらは以前紹介したラジオボタンの入力チェックの記事と同じですね。jsでファイルサイズの入力チェックもいれました。

 

受け取り側の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'] の値を確認
            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('その他のエラーが発生しました');
            }

            // $_FILES['upfile']['mime']の値はブラウザ側で偽装可能なので
            // MIMEタイプを自前でチェックする
            if (!$info = @getimagesize($_FILES['upfile']['tmp_name'][$k])) {
                throw new RuntimeException("[{$k}] 有効な画像ファイルを指定してください");
            }
            //$info[2]はIMAGETYPE_XXX定数
            // 1 IMAGETYPE_GIF
            // 2 IMAGETYPE_JPEG
            // 3 IMAGETYPE_PNG
            if (!in_array($info[2], [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG], true)) {
                throw new RuntimeException("[{$k}] 未対応の画像形式です");
            }
            
            // 横幅330に収まるようにサイズを調整する
            //$info[0]は横幅、$info[1]は高さ
            if($info[0] > 330){
                $dst_width = 330;
                $dst_height = ($info[1] * 330 / max($info[0],1) );
            }else{
                $dst_width = $info[0];
                $dst_height = $info[1];
            }
            
            // 元画像リソースを生成
            if($info[2] == IMAGETYPE_GIF){
                $src = imagecreatefromgif($_FILES['upfile']['tmp_name'][$k]);
            }elseif($info[2] == IMAGETYPE_JPEG){
                $src = imagecreatefromjpeg($_FILES['upfile']['tmp_name'][$k]);
            }elseif($info[2] == IMAGETYPE_PNG){
                $src = imagecreatefrompng($_FILES['upfile']['tmp_name'][$k]);
            }
            
            if(!$src){
                throw new RuntimeException("[{$k}] 画像リソースの生成に失敗しました");
            }
            
            // リサンプリング先画像リソースを生成する
            $dst = imagecreatetruecolor($dst_width, $dst_height);
            
            // getimagesize関数で得られた情報も利用してリサンプリングを行う
            imagecopyresampled($dst, $src, 0, 0, 0, 0, $dst_width, $dst_height, $info[0], $info[1]);

            // 保存先ディレクトリ、ファイル名
            $save_dir = "./upload_image/";
            $filename = date("YmdHis")."_{$k}".image_type_to_extension($info[2]);
            
            // 画像の保存
            if($info[2] == IMAGETYPE_GIF){
                $save_result = imagegif($dst, $save_dir.$filename);
            }elseif($info[2] == IMAGETYPE_JPEG){
                $save_result = imagejpeg($dst, $save_dir.$filename);
            }elseif($info[2] == IMAGETYPE_PNG){
                $save_result = imagepng($dst, $save_dir.$filename);
            }
            
            if(!$save_result){
                throw new RuntimeException("[{$k}] ファイル保存時にエラーが発生しました");
            }

            // 向き修正、exif情報削除
            $org_imagick = new Imagick($_FILES['upfile']['tmp_name'][$k]);
            $org_orientation = $org_imagick->getImageOrientation();
            
            $imagick_outimage = new Imagick($save_dir.$filename);
            
            switch ($org_orientation) {
                case 2: // Mirror horizontal
                    $imagick_outimage->flopImage();
                    break;
                case 3: // Rotate 180
                    $imagick_outimage->rotateImage('#000000', 180);
                    break;
                case 4: // Mirror vertical
                    $imagick_outimage->flipImage();
                    break;
                case 5: // Mirror horizontal and rotate 270
                    $imagick_outimage->flopImage();
                    $imagick_outimage->rotateImage('#000000', 270);
                    break;
                case 6: // Rotate 90
                    $imagick_outimage->rotateImage('#000000', 90);
                    break;
                case 7: // Mirror horizontal and rotate 90
                    $imagick_outimage->flopImage();
                    $imagick_outimage->rotateImage('#000000', 90);
                    break;
                case 8: // Rotate 270
                    $imagick_outimage->rotateImage('#000000', 270);
                    break;
            }
            
            $imagick_outimage->stripimage();
            $imagick_outimage->writeimage($save_dir.$filename);

            $msg = ['green', 'ファイルは正常にアップロードされました'];
            
        } catch (RuntimeException $e) {
            $msg = ['red', $e->getMessage()];
        }
    }
}

少し長いですが、ちょっとずつ見ていきましょう!

 

冒頭1~2行目

if (isset($_FILES['upfile']['error']) && is_array($_FILES['upfile']['error'])) {
    foreach ($_FILES['upfile']['error'] as $k => $error) {

ifは送られてきたファイルの入力チェックです、phpのスーパーグローバル変数$_FILESを崩す攻撃に対する対策のようです。foreachは複数ファイル分POSTされているので、まわしているだけですね

ちなみに$_FILESの中身は以下

$_FILESの中身 説明
$_FILES[‘userfile’][‘name’] クライアントマシンの元のファイル名。
$_FILES[‘userfile’][‘type’] ファイルの MIME 型。ただし、ブラウザがこの情報を提供する場合。 例えば、”image/gif” のようになります。 この MIME 型は PHP 側ではチェックされません。そのため、 この値は信用できません。
$_FILES[‘userfile’][‘size’] アップロードされたファイルのバイト単位のサイズ。
$_FILES[‘userfile’][‘tmp_name’] アップロードされたファイルがサーバー上で保存されているテンポラ リファイルの名前。
$_FILES[‘userfile’][‘error’] このファイルアップロードに関する エラーコード

errorの種類もいくつかあります、以下に載せておきます

errorの種類 説明
UPLOAD_ERR_OK 値: 0; エラーはなく、ファイルアップロードは成功しています。
UPLOAD_ERR_INI_SIZE 値: 1; アップロードされたファイルは、php.ini の upload_max_filesize ディレクティブの値を超えています。
UPLOAD_ERR_FORM_SIZE 値: 2; アップロードされたファイルは、HTML フォームで指定された MAX_FILE_SIZE を超えています。
UPLOAD_ERR_PARTIAL 値: 3; アップロードされたファイルは一部のみしかアップロードされていません。
UPLOAD_ERR_NO_FILE 値: 4; ファイルはアップロードされませんでした。
UPLOAD_ERR_NO_TMP_DIR 値: 6; テンポラリフォルダがありません。PHP 5.0.3 で導入されました。
UPLOAD_ERR_CANT_WRITE 値: 7; ディスクへの書き込みに失敗しました。PHP 5.1.0 で導入されました。
UPLOAD_ERR_EXTENSION 値: 8; PHP の拡張モジュールがファイルのアップロードを中止しました。 どの拡張モジュールがファイルアップロードを中止させたのかを突き止めることはできません。 読み込まれている拡張モジュールの一覧を phpinfo() で取得すれば参考になるでしょう。 PHP 5.2.0 で導入されました。

 

$errorのチェック

if (!is_int($error)) {
    throw new RuntimeException("[{$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('その他のエラーが発生しました');
}

ここは想定されるエラーのチェックです、エラーがあればExceptionを投げます

 

MIMEタイプのチェック

// $_FILES['upfile']['mime']の値はブラウザ側で偽装可能なので
// MIMEタイプを自前でチェックする
if (!$info = @getimagesize($_FILES['upfile']['tmp_name'][$k])) {
    throw new RuntimeException("[{$k}] 有効な画像ファイルを指定してください");
}
//$info[2]はIMAGETYPE_XXX定数
// 1 IMAGETYPE_GIF
// 2 IMAGETYPE_JPEG
// 3 IMAGETYPE_PNG
if (!in_array($info[2], [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG], true)) {
    throw new RuntimeException("[{$k}] 未対応の画像形式です");
}

MIMEタイプをgetimagesizeという関数を使ってチェックします、画像ファイルの大きさなどの情報を取得する関数で返ってきた配列に以下の情報が入っています。

key 説明 その他
0 画像の幅 単位:ピクセル
1 画像の高さ 単位:ピクセル
2 画像の種類を示すフラグ IMAGETYPE定数に対応(ソース中のコメント参照)
3 タグ内でそのまま使用できる文字列 height=”xxx” width=”xxx”
bits ビット/ピクセル PHP4.3以前ではJPEGのみ有効
channels チャンネル数 PHP4.3以前ではJPEGのみ有効
mime 画像のMINEタイプ PHP4.3以降で有効

 

画像のサイズ調整

わたしは今回横幅を330px上限にしましたが、他の条件、サイズにしたければ適宜かえてもらえればOKです。横幅が330超えてたら高さを比で出してるだけですね

// 横幅330に収まるようにサイズを調整する
//$info[0]は横幅、$info[1]は高さ
if($info[0] > 330){
    $dst_width = 330;
    $dst_height = ($info[1] * 330 / max($info[0],1) );
}else{
    $dst_width = $info[0];
    $dst_height = $info[1];
}

 

画像リソースの生成

imagecreatefromxxx(画像の種類によって関数が別)を使って画像リソースを生成します。getimagesizeで取得したIMAGETYPE定数に応じて、関数を分けています。

// 元画像リソースを生成
if($info[2] == IMAGETYPE_GIF){
    $src = imagecreatefromgif($_FILES['upfile']['tmp_name'][$k]);
}elseif($info[2] == IMAGETYPE_JPEG){
    $src = imagecreatefromjpeg($_FILES['upfile']['tmp_name'][$k]);
}elseif($info[2] == IMAGETYPE_PNG){
    $src = imagecreatefrompng($_FILES['upfile']['tmp_name'][$k]);
}

if(!$src){
    throw new RuntimeException("[{$k}] 画像リソースの生成に失敗しました");
}

 

リサンプリング

imagecreatetruecolorという関数で先ほど決めた横幅、高さの画像用リソースを生成して、imagecopyresampled関数を使ってリサンプリングを用いてリサイズします。imagecopyresampledについてはコチラのサイトが分かりやすかったです。

// リサンプリング先画像リソースを生成する
$dst = imagecreatetruecolor($dst_width, $dst_height);

// getimagesize関数で得られた情報も利用してリサンプリングを行う
imagecopyresampled($dst, $src, 0, 0, 0, 0, $dst_width, $dst_height, $info[0], $info[1]);

 

画像の保存

保存先のディレクトリを指定して、ファイル名を付与して保存しています。ファイル名は出来ればユーザー指定の名前じゃなく規則に沿って命名した方がいいのかなーと思います。

// 保存先ディレクトリ、ファイル名
$save_dir = "./upload_image/";
$filename = date("YmdHis")."_{$k}".image_type_to_extension($info[2]);

// 画像の保存
if($info[2] == IMAGETYPE_GIF){
    $save_result = imagegif($dst, $save_dir.$filename);
}elseif($info[2] == IMAGETYPE_JPEG){
    $save_result = imagejpeg($dst, $save_dir.$filename);
}elseif($info[2] == IMAGETYPE_PNG){
    $save_result = imagepng($dst, $save_dir.$filename);
}

if(!$save_result){
    throw new RuntimeException("[{$k}] ファイル保存時にエラーが発生しました");
}

image_type_to_extensionは画像種類に応じて拡張子を取得しているだけ(ドット付き)、画像の保存もまたまたIMAGETYPE定数に応じて関数が別です。

 

exif情報の削除

たぶん↑の時点で消えている気がするのですが、念のため・・・個人情報が特定できるような情報がWebに公開されるとマズイですから。

——-4/25追記——-
・iphoneから画像アップした時に、勝手に横になったりする現象を回避
の修正を入れていた時に気づきましたが、やはりexif情報はありませんでした。詳しくはコードまじえて解説します。

// 向き修正、exif情報削除
$org_imagick = new Imagick($_FILES['upfile']['tmp_name'][$k]);
$org_orientation = $org_imagick->getImageOrientation();

$imagick_outimage = new Imagick($save_dir.$filename);

switch ($org_orientation) {
    case 2: // Mirror horizontal
        $imagick_outimage->flopImage();
        break;
    case 3: // Rotate 180
        $imagick_outimage->rotateImage('#000000', 180);
        break;
    case 4: // Mirror vertical
        $imagick_outimage->flipImage();
        break;
    case 5: // Mirror horizontal and rotate 270
        $imagick_outimage->flopImage();
        $imagick_outimage->rotateImage('#000000', 270);
        break;
    case 6: // Rotate 90
        $imagick_outimage->rotateImage('#000000', 90);
        break;
    case 7: // Mirror horizontal and rotate 90
        $imagick_outimage->flopImage();
        $imagick_outimage->rotateImage('#000000', 90);
        break;
    case 8: // Rotate 270
        $imagick_outimage->rotateImage('#000000', 270);
        break;
}

$imagick_outimage->stripimage();
$imagick_outimage->writeimage($save_dir.$filename);

iphone、ipodで写真を撮ると内部的には横向きなどになって保存されているようです。っで、exif情報のorientationという値で、どの向きが上なの?っていうのを管理しているらしく、サーバ上にファイルを保存する時点で正しい位置を確認しつつ、回転させてから保存、という作りにしました。

exif情報がやはり存在しないことに気づいたのは、82行目のImagick()の画像ファイルを指定するところで、元々は$save_dir.$filenameを指定していました。ただ、コイツはリサンプリングして新たに作り直した画像データ。なので、exif情報が空だったため、orientationはどの画像でも1のまま。そこで、もともとPOSTされてきた画像データの$_FILES[‘upfile’][‘tmp_name’][$k]をImagick()にぶちこんで、getImageOrientation()で見てみたらちゃんと向きの情報(orientation)がありました!
 
なので、もともとPOSTされてきた画像をorgとしてorientationを取得し、それに応じて向きを回転させてから保存するようにしたら出来ました~。結局、stripimage();でexif情報は消してますけどね、念のため( ´・ω・`)

ちなみにorientationの値と補正するための回転方向もまとめておきます

Orientation どう補正すれば正しい向きになるか
1 そのまま
2 上下反転(上下鏡像?)
3 180度回転
4 左右反転
5 上下反転、時計周りに270度回転
6 時計周りに90度回転
7 上下反転、時計周りに90度回転
8 時計周りに270度回転

 

・・・というわけで、こんな感じで出来ました。

わたしはWebに表示する際は、1件ごとにPOSTされたリクエストを管理するDBレコードを用意して、ファイル名を保持しておいて、表示するときはimgタグで画像パスを指定して表示するようにしています。

ほとんどいろんなサイトの記事を参考にしただけですが、細かく詳細を調べていくとたくさんのWebを調べないといけなかったので、わたしのやりたかったことをまとめておきました!まあ、結局↑の中でやりたいことが実現できないとまた調べないといけないのですけどね・・・

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です