概要
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