読者です 読者をやめる 読者になる 読者になる

k-holyのPHPとか諸々メモ

Webで働くk-holyがPHP(スクリプト言語)とか諸々のことをメモしていきます。ソースコードはだいたいWindowsで動かしてます。

画像アップロード(1)Canvasでプレビューして普通に画像をアップロード

JavaScript PHP

AJAXとかドラッグ&ドロップといったややこしい処理はおいといて、HTML5Canvasを使ってクライアント側でプレビューしてみるテストです。

動作確認した環境は以下の通りです。

Volcanus_CanvasResizer.jsでCanvasプレビューする

いきなり俺々ライブラリ登場で恐縮ですが…。

Volcanus_CanvasResizer.js は今回のような処理のために書いた createObjectURL or FileReader + Canvas で画像をリサイズして表示するだけのものです。

/**
 * Volcanus_CanvasResizer
 * 
 * @copyright k-holy <k.holy74@gmail.com>
 * @license The MIT License (MIT)
 */
var Volcanus_CanvasResizer = {
    create: function(args) {
        var instance = {
            file: null
            ,maxWidth: 0
            ,maxHeight: 0
            ,objectUrl: null
            ,onLoad: null
            ,init: function(args) {
                if (typeof args.file !== 'undefined') {
                    this.file = args.file;
                }
                if (typeof args.maxWidth !== 'undefined') {
                    this.maxWidth = args.maxWidth;
                    if (typeof args.maxHeight !== 'undefined') {
                        this.maxHeight = args.maxWidth;
                    }
                }
                if (typeof args.maxHeight !== 'undefined') {
                    this.maxHeight = args.maxHeight;
                    if (typeof args.maxWidth !== 'undefined') {
                        this.maxWidth = args.maxHeight;
                    }
                }
                if (typeof args.onLoad !== 'undefined') {
                    this.onLoad = args.onLoad;
                }
                var url = window.webkitURL || window.URL;
                var createObjectURL = (url && url.createObjectURL);
                var image = new Image();
                var _self = this;
                image.onload = function() {
                    var dist = _self.getSize(image.width, image.height);
                    var canvas = document.createElement('canvas');
                    var context = canvas.getContext('2d');
                    canvas.width = dist.width;
                    canvas.height = dist.height;
                    context.drawImage(image, 0, 0, image.width, image.height, 0, 0, dist.width, dist.height);
                    if (typeof _self.onLoad !== 'undefined') {
                        _self.onLoad(image, canvas);
                    }
                    if (url && _self.objectUrl) {
                        url.revokeObjectURL(_self.objectUrl);
                    }
                };
                if (typeof createObjectURL !== 'undefined') {
                    image.src = this.objectUrl = url.createObjectURL(this.file);
                } else {
                    var reader = new FileReader();
                    reader.onload = function(event) {
                        image.src = event.target.result;
                    }
                    reader.readAsDataURL(this.file);
                }
                return this;
            }
            ,getSize: function(srcWidth, srcHeight) {
                var dist = {
                    width: srcWidth
                    ,height: srcHeight
                }
                if ((this.maxWidth > 0 && srcWidth > this.maxWidth) || (this.maxHeight > 0 && srcHeight > this.maxHeight)) {
                    var w_percent = (100 * this.maxWidth) / srcWidth;
                    var h_percent = (100 * this.maxHeight) / srcHeight;
                    if (w_percent < h_percent) {
                        dist.width = this.maxWidth;
                        dist.height = Math.floor((srcHeight * w_percent) / 100);
                    } else {
                        dist.width = Math.floor((srcWidth * h_percent) / 100);
                        dist.height = this.maxHeight;
                    }
                    if (dist.width < 1) {
                        dist.width = 1;
                    }
                    if (dist.height < 1) {
                        dist.height = 1;
                    }
                }
                return dist;
            }
        }
        instance.init(args);
        return instance;
    }
};

以下はこのライブラリを使ったHTML + JavaScriptだけの最小サンプルです。

<!DOCTYPE html>
<html lang="ja">

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="//code.jquery.com/jquery-1.11.2.min.js"></script>
<title>CanvasResizer 最小サンプル</title>
</head>

<body>
<form method="post" enctype="multipart/form-data">
    <div class="uploader">
        <p>
            <input name="sample_image" type="file" accept="image/*" />
        </p>
        <div class="thumbnail"></div>
    </div>
    <p>
        <input type="submit" value="送信する" />
    </p>
</form>

<script type="text/javascript" src="/js/Volcanus/CanvasResizer.js"></script>
<script type="text/javascript">/*<![CDATA[*/
$(function() {
    $('.uploader input[type="file"]').on('change', function(event) {
        if (event.target.files.length === 0) {
            return;
        }
        var file = event.target.files[0]
        if (file.type.substring(0, 6) === 'image/') {
            var uploader = $(this).parent().parent()[0];
            var thumbnail = Volcanus_CanvasResizer.create({
                file: file
                ,maxWidth: 400
                ,maxHeight: 400
                ,onLoad: function(image, canvas) {
                    var lastModifiedDate = '';
                    if (typeof file.lastModifiedDate !== 'undefined') {
                        lastModifiedDate = file.lastModifiedDate.toUTCString();
                    }
                    $(uploader).find('.thumbnail').html(
                        $('<img>').attr('src', canvas.toDataURL('image/png'))
                    ).append(
                        $('<h3>').text(file.name)
                    ).append(
                        $('<ul>')
                            .append($('<li>').text('ファイルサイズ:' + file.size))
                            .append($('<li>').text('ファイル種別:' + file.type))
                            .append($('<li>').text('横幅:' + image.width))
                            .append($('<li>').text('高さ:' + image.height))
                            .append($('<li>').text('更新日時:' + lastModifiedDate))
                    );
                }
            });
        }
    });
});
/*]]>*/</script>

</body>
</html>

Volcanus_CanvasResizer.create({...}) で対象ファイル、横幅の最大値、高さの最大値、画像ファイルを読み終えた際のコールバックを指定しています。

コールバックの引数に元画像のImageオブジェクトとリサイズされたCanvasが渡されますので、 Canvas.toDataURL() でDataURLに変換してプレビュー表示したり、元の元の横幅、高さなどを取得できます。

フォーム上でファイルを選択すると、こんな感じで指定した横幅と高さの最大値に合わせてリサイズされたプレビュー画像が表示されます。

クライアントのメモリ量に左右されますので、巨大なファイルだとこういうことになります…。(私の拙いJavaScript力だとこんなもんです…)

Volcanus_FileUploaderでPHPのファイルアップロード処理を簡潔に書く

またまた俺々ライブラリ登場で恐縮ですが…。

Volcanus_FileUploader は今回のような処理のために書いた…(略)

Uploaderクラス + FileValidatorクラス + Fileクラス (FileInterface実装クラス) という構成になっていて、これらを組み合わせて使います。

index.php

<?php
/**
 * Volcanus_CanvasResizer.js + Volcanus_FileUploader
 *
 * @copyright k-holy <k.holy74@gmail.com>
 * @license The MIT License (MIT)
 */
include __DIR__ . '/../../vendor/autoload.php';

use Volcanus\FileUploader\Uploader;
use Volcanus\FileUploader\FileValidator;
use Volcanus\FileUploader\File\NativeFile;
use Volcanus\FileUploader\Exception\FilenameException;
use Volcanus\FileUploader\Exception\FilesizeException;
use Volcanus\FileUploader\Exception\ExtensionException;
use Volcanus\FileUploader\Exception\ImageWidthException;
use Volcanus\FileUploader\Exception\ImageHeightException;

$form = [];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    $uploader = new Uploader([
        'moveDirectory' => '/path/to/temp/files',
        'moveRetry' => 1,
    ]);

    $validator = new FileValidator([
        'filenameEncoding' => 'UTF-8',
        'allowableType' => 'jpg,png',
        'maxWidth' => 400,
        'maxHeight' => 400,
        'maxFilesize' => '1M',
    ]);

    if (isset($_FILES['sample_image'])) {

        $uploadedFile = new NativeFile($_FILES['sample_image']);

        try {

            $uploader->validate($uploadedFile, $validator);

            $form['name'] = $uploadedFile->getClientFilename();
            $form['size'] = $uploadedFile->getSize();
            $form['type'] = $uploadedFile->getMimeType();
            $form['dataUri'] = $uploadedFile->getContentAsDataUri();

            if ($uploadedFile->isImage()) {
                if (false !== (list($width, $height, $type, $attr) = getimagesize($uploadedFile->getPath()))) {
                    $form['width'] = $width;
                    $form['height'] = $height;
                }
            }

            $form['movedPath'] = $uploader->move($uploadedFile);

        } catch (FilenameException $e) {
            $error = 'ファイル名が不正です。';
        } catch (FilesizeException $e) {
            $error = sprintf('ファイルサイズが%sバイトを超えています。', $validator->config('maxFilesize'));
        } catch (ExtensionException $e) {
            $error = sprintf('アップロード可能なファイルは%sです。', $validator->config('allowableType'));
        } catch (ImageWidthException $e) {
            $error = sprintf('横幅が%spxを超えています。', $validator->config('maxWidth'));
        } catch (ImageHeightException $e) {
            $error = sprintf('高さが%spxを超えています。', $validator->config('maxHeight'));
        } catch (\Exception $e) {
            $error = 'アップロードに失敗しました。';
        }
    }

}
include __DIR__ . '/template.php';

バリデーションはFileValidatorクラスで受け付けるファイルタイプやファイルサイズ、画像の横幅や高さを指定した上で、Uploader::validate() の第1引数にFile、第2引数にFileValidatorを渡して実行します。

バリデーションエラーは例外でスローされますので、適宜これをキャッチすることでエラーメッセージを出力できます。(複数の項目にバリデーションエラーがある場合は最初に検知したもののみとなります…ここはいずれ変えるかも…)

※2015-03-19追記:最新版ではバリデーションエラー発生時に例外をスローするかどうかをオプションで指定できるようにしました。

上記サンプルでは、バリデーション結果がOKの場合のみ、DataURI形式で画像を取得するとともに、ファイルを移動して移動先のパスを取得しています。この辺りは画像ファイルのアップロードではお決まりの処理になるんじゃないでしょうか。(ファイルが巨大だとDataURI生成でメモリがアウアウしてしまうかもしれませんが…)

前述の Volcanus_CanvasResizer.js に加えて、上記サーバ側で取得した変数の表示を組み込むとこんな感じです。(素のPHP

template.php

<!DOCTYPE html>
<html lang="ja">

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="//code.jquery.com/jquery-1.11.2.min.js"></script>
<title>CanvasResizer + Volcanus_FileUploaderサンプル</title>
</head>

<body>
<?php if (isset($error)) : ?>
    <p><strong><?=htmlspecialchars($error)?></strong></p>
<?php endif ?>
<form method="post" enctype="multipart/form-data">

    <div class="uploader">
        <p>
            <input name="sample_image" type="file" accept="image/*" />
        </p>
        <div class="thumbnail">
<?php if (isset($form['dataUri'])) : ?>
            <img src="<?=htmlspecialchars($form['dataUri'])?>" />
<?php else : ?>
            <img data-src="/js/holder.js/400x400" />
<?php endif ?>
<?php if (isset($form['name'])) : ?>
            <h3><?=htmlspecialchars($form['name'])?></h3>
<?php endif ?>
            <ul>
<?php if (isset($form['size'])) : ?>
                <li>ファイルサイズ:<?=htmlspecialchars($form['size'])?></li>
<?php endif ?>
<?php if (isset($form['type'])) : ?>
                <li>ファイル種別:<?=htmlspecialchars($form['type'])?></li>
<?php endif ?>
<?php if (isset($form['width'])) : ?>
                <li>横幅:<?=htmlspecialchars($form['width'])?></li>
<?php endif ?>
<?php if (isset($form['height'])) : ?>
                <li>高さ:<?=htmlspecialchars($form['height'])?></li>
<?php endif ?>
<?php if (isset($form['movedPath'])) : ?>
                <li>配置先:<?=htmlspecialchars($form['movedPath'])?></li>
<?php endif ?>
            </ul>
        </div>
    </div>

    <p>
        <input type="submit" value="送信する" />
    </p>

</form>

<script type="text/javascript" src="/js/Volcanus/CanvasResizer.js"></script>
<script type="text/javascript">/*<![CDATA[*/
$(function() {
    $('.uploader input[type="file"]').on('change', function(event) {
        if (event.target.files.length === 0) {
            return;
        }
        var file = event.target.files[0]
        if (file.type.substring(0, 6) === 'image/') {
            var uploader = $(this).parent().parent()[0];
            var thumbnail = Volcanus_CanvasResizer.create({
                file: file
                ,maxWidth: 400
                ,maxHeight: 400
                ,onLoad: function(image, canvas) {
                    var lastModifiedDate = '';
                    if (typeof file.lastModifiedDate !== 'undefined') {
                        lastModifiedDate = file.lastModifiedDate.toUTCString();
                    }
                    $(uploader).find('.thumbnail').html(
                        $('<img>').attr('src', canvas.toDataURL('image/png'))
                    ).append(
                        $('<h3>').text(file.name)
                    ).append(
                        $('<ul>')
                            .append($('<li>').text('ファイルサイズ:' + file.size))
                            .append($('<li>').text('ファイル種別:' + file.type))
                            .append($('<li>').text('横幅:' + image.width))
                            .append($('<li>').text('高さ:' + image.height))
                            .append($('<li>').text('更新日時:' + lastModifiedDate))
                    );
                }
            });
        }
    });
});
/*]]>*/</script>

</body>
</html>

バリデーションエラーの時はこんな感じでメッセージ表示しています。

バリデーションエラーがなければアップロードファイルの内容をDataURIで取得した後でファイルを移動し、DataURIの画像と移動先のパスを表示しています。

なお移動後のファイル名は sha1(uniqid(mt_rand(), true)) とアップロード時の拡張子で決定されます。

確認画面を入れたい、ファイル選択がださいのでスタイル適用したい、ドラッグ&ドロップ対応、かつIEでも動くように、など様々な要求に対応しようとすると大変ですが、ひとまずクライアント側プレビューとサーバ側バリデーションとアップロードのシンプルな流れは実装できました。

参考にした情報