のんラボ

今更だけどCropper.jsでの画像トリミングについて書いてみました!

2019/05/10 2019/05/10

WEBサービスにおける画像トリミングについて考えてみた。 とある自作WEBサービスの開発中、ユーザーアイコンを登録できるようにしよう!と考えつきアップロードしたはいいものの、やっぱりトリミングできた方がいいよね。と考えGoogle先生に色々聞いてみました。 https://fengyuanchen.github.io/cropperjs/ https://fengyuanchen.github.io/cropperjs/を見る なるものを発見したのはいいのですが、そこで苦労したので備忘録ついでに書いてみました。 留意点 これまでにもCropperでのサンプルは色々上がっておりましたが、またそれとは違うパターンにしておいたはずですので参考になればと思います。 サーバーサイドについては触れていません。 サンプル https://labo.nozomi.bike/cropper/sample.html https://labo.nozomi.bike/cropper/sample.htmlを見る <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に喰われている感がある今日のこの頃です。 何か指摘事項ありましたら是非ともお願いいたします。 何分ぺーぺーなもので、指摘されるために書いたまであります。 参考 cropperjs/README.md at main · fengyuanchen/cropperjs JavaScript image cropper. Contribute to fengyuanchen/cropperjs development by creating an account on...

non's Laboをリニューアルしました!

2019/05/03 2019/05/03

non's Laboをリニューアルしました! 皆さんお久しぶりです。のんです。 知っている人は知っているnons'Labo。 大幅アップデートしましたー!! なぜリニューアルしたのか? non-it-infomation.comって思いっきりTypoした状態。 これで3年間近く使用してきました。 実はすぐに気づいていて、どうやって変えようか迷っていたのですが、どうも変更方法がわからない。 なので、放置していました。 ツーマッチ開発のために取得したnozomi.bikeドメインにすべて移行しようと思ったのは半年くらい前。non-it-infomation.comはAPI用とかなにか別の目的で使用かなと思っています。んで期限が切れたらそのまま解約しようかなと。 実はnon's Laboは処女作 個人開発で作成したという意味では、処女作だったんです。 当初はPHP + Smarty2っていうプレーンな状態で開発を進めていました。当時は新卒の頃だったので、プログラミングの知識は皆無(自己紹介でもあるようにもともと工学の人間)ですので、クラスの勉強やら、Html CSSの勉強やらをするために作成したブログでした。そこからWordPress風にしたり、動画と画像をアップロードできるようにしたり色々いじっていました。 しかし、そろそろ技術もついてきましたし、オンボロのこの子をなんとかピカピカにしてあげようとして、作成したのがこのアップデートです。 新しい技術を取り入れたかった結果、安定しなかった。 もともとの技術がレガシーなコードで構成されていましたので、なにかフレームワークやライブラリを入れようと、いろいろごちゃごちゃ入れまくった結果。もう汚い汚い。しかし、これも僕の資産。棚卸しを先日完了したので、作業に移った次第です。 ちなみに、non's Labo、長いこと続けてきたおかげで、SEO的に優秀(らしい)です。 少なくともnons laboで検索をしたTOPに表示されます。 だからドメインも捨てたくなかったんですが、これはしょうがない。SEOの勉強ということで色々実験してみましょう。 目的 目的って見出しをブログのエディターに書くと初代non's Laboで直に書いたの思い出しますねw 目的はあの頃とは違います。(ちなみに、あの頃の目的) 技術のアウトプットを習慣にする ブログを書くことで怠け癖を治す みんなに知られるブログにする あの頃の目的は自分のためだったけど、今度の目的はみんなのためって感じですね。このブログ見て悩みが解決したとかならとても嬉しいと思います。 使用技術 最近はこれで個人開発を行っています。慣れているので、開発スピードが早いんですよね。勉強が目的ではないので、下記を選択。 Laravel Vue このブログについて一言。 最初はブログサービスなんて山程あるし、外部のブログでやろうかなと思ったんですよ。でもQiitaなどの記事形式以外は長いこと続かなくて、、、 ちなみにWordPressを使用してみましたが、僕には合いませんでした。 多分、書かされている感あるんですよね。。。 それに比べて自作のものは使ってて嬉しいですよね。ちゃんと動いてるって感じを書きながら感じられる。きっと今のバージョンのnon's Laboにも使いにくいところがあるかもしれませんが、それを直すのも醍醐味というか楽しいところですよね。 ということでブログを書いていく中で原則を決めました。 楽しくブログを書く です!。当たり前かと思われそうですが、僕ってそもそもブログ書くのに向いてない。。。 最後に  個人開発のアプリも、連続したアップデートでユーザーさんには迷惑をかけているかもしれません。 全てはもっと良くしていこうという結果なのでご容赦を。 3度めのアップデートですが、今後ともよろしくお願いいたします。