WEBサービスにおける画像トリミングについて考えてみた。
とある自作WEBサービスの開発中、ユーザーアイコンを登録できるようにしよう!と考えつきアップロードしたはいいものの、やっぱりトリミングできた方がいいよね。と考えGoogle先生に色々聞いてみました。
なるものを発見したのはいいのですが、そこで苦労したので備忘録ついでに書いてみました。
留意点
- これまでにもCropperでのサンプルは色々上がっておりましたが、またそれとは違うパターンにしておいたはずですので参考になればと思います。
- サーバーサイドについては触れていません。
サンプル
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="dist/cropper.css" charset="UTF-8">
<title>Cropper Sample</title>
<style>
/* 下記は円形にするなら必須です。 */
.cropper-view-box,
.cropper-face {
border-radius: 50%;
}
/* 下記はできれば必要なスタイルかと思います。(厳密にはスタイルなど必要ありませんが、最低現のスタイルとしてという意味です。) */
.cropper-container{
width: 100%;
}
/* 下記は必須ではありません。 Sampleを見やすくするために作成しました。 */
main{
width: 50%;
margin: 0 auto;
}
main .triming-image{
width: 100%;
height: 100px;
border: dashed #000 1px;
cursor: pointer;
}
main #trimed_image{
height: 500px;
}
</style>
</head>
<body>
<main>
<h1>Cropper Sample</h1>
<p>ここでのサンプルはTwitterアイコン風に丸型とします。</p>
<p>特にCSSに指定はありません。</p>
<p>各自で自由に設定してください。</p>
<div class="cropper-container">
<p><input type="file" id="triming_image" name="triming_image" class="triming-image" required /></p>
<p><img src="" alt="トリミング画像" id="trimed_image" style="display: none;" /></p>
<p><input type="button" id="crop_btn" value="画像をトリミングして送信" /></p>
</div>
<p>トリミング結果が下記に表示されます。</p>
<p>例ではAjaxにて送信済みでので、下記機能に特に意味がありません。</p>
<p>(結果表示したところですでに送信済みですので。)</p>
<p>Cropper.jsそのものに画像操作は<a href="https://fengyuanchen.github.io/cropper/" target="_blank">https://fengyuanchen.github.io/cropper/</a></p>
<div id="result"></div>
</main>
</body>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js" charset="UTF-8"></script>
<script type="text/javascript" src="dist/cropper.js" charset="UTF-8"></script>
<script type="text/javascript">
/**
* 丸くトリミングするために必要な関数です。
* キャンバスの画像を円形に座標計算し、切り取って返しています。
*/
function getRoundedCanvas(sourceCanvas) {
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
var width = sourceCanvas.width;
var height = sourceCanvas.height;
canvas.width = width;
canvas.height = height;
context.imageSmoothingEnabled = true;
context.drawImage(sourceCanvas, 0, 0, width, height);
context.globalCompositeOperation = 'destination-in';
context.beginPath();
context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true);
context.fill();
return canvas;
}
$(function(){
$('#triming_image').on('change', function(event){
var trimingImage = event.target.files;
// imageタグは1つしかファイルを送信できない仕組みと複数送信する仕組みの二通りありますので、サーバー側でチェックを忘れないようにしてください。
if(trimingImage.length > 1){
console.log(trimingImage.length + 'つのファイルが選択されました。');
return false;
}
// 改め代入します。
trimingImage = trimingImage[0];
// 画像のチェックを行いますが、あくまでjsでのチェックなのでサーバーサイドでもう一度チェックを行ってください。
if(!trimingImage.type.match('image/jp.*') // jpg jpeg でない
&&!trimingImage.type.match('image/png') // png でない
&&!trimingImage.type.match('image/gif') // gif でない
&&!trimingImage.type.match('image/bmp') // bmp でない
){
alert('No Support ' + trimingImage.type + ' type image');
$(this).val('');
return false;
}
var fileReader = new FileReader();
fileReader.onload = function(e){
var int32View = new Uint8Array(e.target.result);
// see https://en.wikipedia.org/wiki/List_of_file_signatures
// ファイルのヘッダを参照し、マイムタイプを疑似的に取得します。フレームワークによってはもっと簡単に正確に読めるものもあります。
// 下記は厳しい設定です。正規の手順を踏んでもアップロードできないカメラなどがあります。
// (私の環境ではアクションカメラの写真などは下記に引っ掛かりました。)
if((int32View.length>4 && int32View[0]==0xFF && int32View[1]==0xD8 && int32View[2]==0xFF && int32View[3]==0xE0)
|| (int32View.length>4 && int32View[0]==0xFF && int32View[1]==0xD8 && int32View[2]==0xFF && int32View[3]==0xDB)
|| (int32View.length>4 && int32View[0]==0xFF && int32View[1]==0xD8 && int32View[2]==0xFF && int32View[3]==0xD1)
|| (int32View.length>4 && int32View[0]==0x89 && int32View[1]==0x50 && int32View[2]==0x4E && int32View[3]==0x47)
|| (int32View.length>4 && int32View[0]==0x47 && int32View[1]==0x49 && int32View[2]==0x46 && int32View[3]==0x38)
|| (int32View.length=2 && int32View[0]==0x42 && int32View[1]==0x4D && int32View[2]==0x46 && int32View[3]==0x38)
){
// success
$('#trimed_image').css('display', 'block');
$('#trimed_image').attr('src', URL.createObjectURL(trimingImage));
return true;
} else {
// failed
alert('No Support ' + trimingImage.type + ' type image');
// exeファイルのアップロードを考えると下記よりもいいプラクティスがある可能性があります。
$('#trimed_image').val('');
return false;
}
};
fileReader.readAsArrayBuffer(trimingImage);
fileReader.onloadend = function(e){
var image = document.getElementById('trimed_image');
var button = document.getElementById('crop_btn');
var croppable = false;
var cropper = new Cropper(image, {
aspectRatio: 1,
viewMode: 1,
ready: function () {
croppable = true;
},
});
// fileReaderが完了した後にボタンクリックイベントを作成する必要があります。
button.onclick = function () {
var croppedCanvas;
if (!croppable) {
alert('トリミングする画像が設定されていません。');
return false;
}
// cropper.jsに用意されている機能です。
croppedCanvas = cropper.getCroppedCanvas();
// 下記toBlob関数はブラウザによって名前が違います。
var blob;
if(croppedCanvas.toBlob){
croppedCanvas.toBlob(function(blob){
var trimedImageForm = new FormData();
trimedImageForm.append('blob', blob);
// この例ではAjaxにて送信します。
$.ajax({
url: '', // POST送信先
type: 'post',
processData: false,
contentType: false,
data: trimedImageForm,
}).done(function( jsonResponse ){
var responese = $.parseJSON(jsonResponse);
if(responese.status == 'success'){
console.log(responese);
alert('アップロードしました。');
}else if(responese.status == 'error'){
alert('画像作成に失敗しました。再度お試しください。\n' + responese.msg);
}else{
alert('システムエラーが発生しました。');
}
}).fail(function( responese ) {
alert('システムエラーが発生しました。');
// フレームワークによってはサーバーエラーをjsonで返してくれます。
var responese = $.parseJSON(jsonResponse);
});
});
}else if(croppedCanvas.msToBlob){
blob = croppedCanvas.msToBlob();
var trimedImageForm = new FormData();
trimedImageForm.append('blob', blob);
// この例ではAjaxにて送信します。
$.ajax({
url: '', // POST送信先
type: 'post',
processData: false,
contentType: false,
data: trimedImageForm,
}).done(function( jsonResponse ){
var responese = $.parseJSON(jsonResponse);
if(responese.status == 'success'){
console.log(responese);
alert('アップロードしました。');
}else if(responese.status == 'error'){
alert('画像作成に失敗しました。再度お試しください。\n' + responese.msg);
}else{
alert('システムエラーが発生しました。');
}
}).fail(function( responese ) {
alert('システムエラーが発生しました。');
// フレームワークによってはサーバーエラーをjsonで返してくれます。
var responese = $.parseJSON(jsonResponse);
});
}else{
// これは少しわからないです。申し訳ない。
imageURL = canvas.toDataURL();
}
// 画面にトリミング結果を出力する場合は下記が必要です。
// 例ではAjaxにて送信済みでので、下記機能に特に意味がありません。(結果表示したところですでに送信済みですので。)
var result = document.getElementById('result');
var roundedImage;
roundedCanvas = getRoundedCanvas(croppedCanvas);
roundedImage = document.createElement('img');
roundedImage.src = roundedCanvas.toDataURL()
roundedImage.name = 'trimed';
roundedImage.id = 'trimed';
result.innerHTML = '';
result.appendChild(roundedImage);
};
};
});
});
</script>
</html>
<?php
$res = array();
try{
// $_FILESで受け取れます。
$file = $_FILES['blob'];
// 画像アップロードについては割愛。
$res = [
'status' => 'success',
'msg' => 'sample01',
'obj' => $file,
];
}catch(Exception $ex){
$res = [
'status' => 'error',
'msg' => $ex->getMessge(),
'obj' => null,
];
}
echo json_encode($res);
exit();
ちょっと冗長な箇所ありますが、result部分など省けばもう少しシンプルにまとまりますね。
画像選択->トリミング->アップロードまでを簡単に行いたいときには便利ではないでしょうか。
課題
- Cropperの再読み込み(画像間違えちゃったー)のパターンからの復帰方法
- これは公式ページを見れば改善可能ですね。
- toBlob関数のブラウザ依存
- 3通り書きましたがtoBlobとmsToBlob以外にあるのか?しかもすべてに当てはまらない場合どのようにアップすればいいかわかりませんでした。知識のある方ご教示いただきたい。
画像トリミングの重要性
最後になりますが、画像トリミング、かなり重要だと思うんですよね。
WEBサービスだと画像アップしてもトリミングさせてくれないサービスが多いと思います。
セキュリティなども重要ですが、ユーザビリティも重要ですよね。
有名どころであれば(Google,Twitter,GitHub……)色々なアカウントサービスではアイコンをトリミングさせてくれますが、
ぶっちゃけ小さな会社のサービスとか含めてしまいますとどうなのでしょうか。
こういったコンテンツ編集系の処理はあまり得意としませんが、Youtube等の台頭で動画の編集もWEBやるのが当たり前になるかもしれませんね。(Twitterとかそうだし、もうなっているのか?)
ネイティブで組まなきゃーってなっていたところがWEBに喰われている感がある今日のこの頃です。
何か指摘事項ありましたら是非ともお願いいたします。
何分ぺーぺーなもので、指摘されるために書いたまであります。