LoginSignup
33
26

More than 5 years have passed since last update.

JavaScriptで画像をリサイズしてから画像をアップロードする

Last updated at Posted at 2017-11-19

概要

canvasで画像を作ることが出来ることは知っていたので
サーバー側で画像のリサイズを行わないでJavaScriptでリサイズしてから画像を送信する機能を作ってみようと思い、作成してみました。

ポイント

input type file の 値はjavascriptで上書き出来ないため、設定したファイルをリサイズしたものに変更して送信するためにAjaxを使用しています。

クライアントサイド

<!DOCTYPE html>
<html>
<head>
<script>
var scaleSlider = null;
var scaleValue = null;
var canvas = null;
var context = null;
var image = null;
var fixFileObject = null;
var scale = null;
var messageArea = null;
var csrf_token_name = "{$csrf_token_name}";
var csrf_hash = "{$csrf_hash}";

document.addEventListener("DOMContentLoaded", function() {
    canvas = document.getElementById("thumbnail");
    context = canvas.getContext("2d");

    scaleSlider = document.getElementById("scale");
    scaleValue = document.getElementById("scale-value");

    messageArea = document.getElementById("message")

    scaleSlider.addEventListener("change", changeScale);

    setFileEventListenner();
});

function changeScale() {
    scaleValue.value = scaleSlider.value;
    drawCanvas();
}

function setFileEventListenner() {
    document.getElementById("file-selecter").addEventListener("change", createPreview);
}

function createPreview(event) {

    fixFileObject = null;

    var fileObject = event.target.files[0];

    if (typeof fileObject === "undefined") {
        return;
    }

    if (fileObject.type.match(/^image\/(jpeg|png)$/) === null) {
        // jpegとpng以外の場合はクリアして終了
        var fileArea = document.getElementById("file-input");
        fileArea.innerHTML = fileArea.innerHTML;
        setFileEventListenner();
        return;
    }

    fixFileObject = fileObject;

    image = new Image();

    var reader = new FileReader();

    reader.onload = (function(fileObject) {
        return function(event) {
            image.src = event.target.result;// base64
        };
    })(fileObject);

    image.onload = function() {
        drawCanvas();
    }

    reader.readAsDataURL(fileObject);
}

function drawCanvas() {
    scale = scaleSlider.value;
    if (image !== null) {
        var imageWidth = parseInt(image.width * scale, 10);
        var imageHeight = parseInt(image.height * scale, 10);
        canvas.width = imageWidth;
        canvas.height = imageHeight;
        context.clearRect(0, 0, imageWidth, imageHeight);
        context.drawImage(image, 0, 0, imageWidth, imageHeight);
    }
}

function submitResizeFile() {
    if (image !== null && fixFileObject !== null) {

        var resizeFileObject = null;

        if (scale !== 1) {
            // サイズ変更があった場合だけ送信用ファイルを作成
            var image64Data = canvas.toDataURL(fixFileObject.type);
            image64Data = image64Data.split(',')[1];
            imageData = atob(image64Data);
            var unit8Array = new Uint8Array(imageData.length);
            unit8Array.forEach(function(element, index) {
                unit8Array[index] = imageData.charCodeAt(index);
            });
            resizeFileObject = new File(
                [unit8Array],
                fixFileObject.name,
                {
                    type: fixFileObject.type
                }
            );
        } else {
            // サイズ変更がない場合はそのまま
            resizeFileObject = fixFileObject;
        }

        var formData = new FormData();
        formData.append(csrf_token_name, csrf_hash);
        formData.append("file", resizeFileObject);

        // input type file の 値はjavascriptで上書き出来ないのでajaxで送信する
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (xhr.readyState === XMLHttpRequest.DONE) {
                if (xhr.status === 200) {
                    csrf_hash = JSON.parse(xhr.response).csrf_hash;
                    messageArea.innerHTML = '<span style="color: green;">ファイルの送信に成功しました。</span>';
                } else {
                    messageArea.innerHTML = '<span style="color: red;">ファイルの送信に失敗しました。</span>';
                }
            }
        }
        xhr.open("POST", "/root/execute_upload");
        xhr.send(formData);
    }
}
</script>
</head>
<body>
<form id="form">
    Scale : <input id="scale" type="range" step="0.1" min="0.1" max="1" value="1"><input disabled id="scale-value" type="text" value="1" style="width: 2rem;">
    <div id="file-input"><input id="file-selecter" type="file" name="file" accept="image/jpeg, image/png"></div>
    <div><canvas id="thumbnail" width="10" height="10"></canvas></div>
    <input type="button" value="送信" onclick="submitResizeFile()">
    <div id="message"></div>
</form>
</body>
</html>

サーバーサイドは無いので失敗しますが、以下のような感じになります。
(CSRFの処理はコメントアウトしています。)
https://jsfiddle.net/bfb4b39z/2/

サーバーサイド(ファイル受け取り部分 CodeIgniterのコントローラ)

public function execute_upload()
{
    $data = [];
    $data['csrf_token_name'] = $this->security->get_csrf_token_name();
    $data['csrf_hash'] = $this->security->get_csrf_hash();

    try
    {
        if ( ! empty($_FILES))
        {
            // ファイルは複数ではない
            if (count($_FILES['file']['name']) > 1)
            {
                // error処理
                throw new RuntimeException('ファイルが複数設定されています。');
            }

            // エラーがない
            switch ($_FILES['file']['error'])
            {
                case UPLOAD_ERR_OK:
                    break;
                case UPLOAD_ERR_INI_SIZE:
                case UPLOAD_ERR_FORM_SIZE:
                    throw new RuntimeException('ファイルのサイズが大きるためアップロードされませんでした。');
                case UPLOAD_ERR_PARTIAL:
                    throw new RuntimeException('ファイルは完全にアップロードされませんでした。');
                case UPLOAD_ERR_NO_FILE:
                    throw new RuntimeException('ファイルはアップロードされませんでした。');
                case UPLOAD_ERR_NO_TMP_DIR:
                    throw new RuntimeException('テンポラリフォルダがありません。');
                case UPLOAD_ERR_CANT_WRITE:
                    throw new RuntimeException('ディスクへの書き込みに失敗しました。');
                case UPLOAD_ERR_EXTENSION:
                    throw new RuntimeException('異常が発生したためアップロードに失敗しました。');
            }

            // MIMEチェック
            $mime_type = mime_content_type($_FILES['file']['tmp_name']);
            $match_result = preg_match('/^image\/(jpeg|png)$/', $mime_type);
            if ($match_result === 0 || $match_result === false)
            {
                throw new RuntimeException('許可されていないファイル形式です。');
            }

            $uploaddir = APPPATH . '../public/uploads/';
            $uploadfile = $uploaddir . basename($_FILES['file']['name']);

            if (move_uploaded_file($_FILES['file']['tmp_name'], $uploadfile) === false) {
                throw new RuntimeException('ファイルのアップロードに失敗しました。');
            }

            chmod($uploadfile, 0744);
        }
        else
        {
            throw new RuntimeException('ファイルが設定されていません');
        }
    }
    catch (RuntimeException $exception)
    {
        log_message('error', $exception);
        set_status_header(500);
    }

    header('Content-Type: application/json');
    echo json_encode($data);
}

Codeigniterでは「ファイルアップロードクラス」が使えるのでtry catchは以下で置き換えできます

try
{
    // アップロード設定
    $config['upload_path'] = APPPATH . '../public/uploads/';
    $config['allowed_types'] = 'jpg|png';
    $config['file_name'] = 'images';// 拡張子を省略すると元々の拡張子で保存してくれます。
    $config['overwrite'] = true;

    $this->load->library('upload', $config);

    // アップロード
    if ( ! $this->upload->do_upload('file'))
    {
        throw new RuntimeException($this->upload->display_errors());
    }
}
catch (RuntimeException $exception)
{
    log_message('error', $exception);
    set_status_header(500);
}

メモ

ファイルが複数で指定された場合(name="file[]" multiple="multiple"で複数送信した場合)
var_dump($_FILES);は以下のようになります。

array(1) { 
    ["file"]=> array(5) {
        ["name"]=> array(2) {
            [0]=> string(12) "test.jpg"
            [1]=> string(18) "test2.png"
        }
        ["type"]=> array(2) {
            [0]=> string(10) "image/jpeg"
            [1]=> string(9) "image/png"
        }
        ["tmp_name"]=> array(2) {
            [0]=> string(14) "/tmp/phpwnzE0w"
            [1]=> string(14) "/tmp/phpXa5NLJ"
        }
        ["error"]=> array(2) {
            [0]=> int(0)
            [1]=> int(0)
        }
        ["size"]=> array(2) {
            [0]=> int(9357)
            [1]=> int(35964)
        }
    }
}

参考

ファイルを参照させサムネイルを出す
https://www.html5rocks.com/ja/tutorials/file/dndfiles/

キャンバスから画像を作り出す
http://www.pori2.net/html5/Canvas/150.html

base64をファイルオブジェクトにする
https://stackoverflow.com/questions/16968945/convert-base64-png-data-to-javascript-file-objects

xhr
https://stackoverflow.com/questions/3038901/how-to-get-the-response-of-xmlhttprequest

$_FILES
http://php.net/manual/ja/features.file-upload.post-method.php

33
26
0

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
33
26