Edited at

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

More than 1 year has passed since last update.


概要

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