FileAPIとcanvasを使ってアカウント画像の登録機能を作る

FileAPIとcanvasを使ってアカウント画像の登録機能を作る

FileAPIで画像の選択は簡単にできる

React使ってSPAを作るよ(21)の続きですが、今回はReactはあんまり関係ありません。
アカウント登録・更新のときにアイコン画像を設定できる機能を追加したいと思います。

こんな感じにしていきますよ~

fileapi

まずはHTMLとCSSで要素を作っていきます。

<div class="media-select">
	<canvas class="media-canvas"></canvas>
	<label class="media-label" style="background-image: url(/images/account/default.png);">
		<input type="file" class="media-input" accept="image/*" form="">
	</label>
	<button type="button" class="media-delete" style="display: none;"></button>
	<input type="hidden" class="media-base64" name="media" value="h/images/account/default.png">
</div>

画像を登録しなかった場合のデフォルトの画像のパスを入れておくようにします。

/*-- アイコン画像登録フォーム --*/

.media-select {
  position: relative;
  width: 80px;
  height: 80px;
  margin: 10px auto 0 auto;
}

.media-input {
  position: fixed;
  top: -100%;
  right: 100%;
}

.media-label {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 5px;
  background: url(/images/account/default.png) no-repeat 50% 50%;
  background-size: cover;
}

.media-delete {
  cursor: pointer;
  position: absolute;
  top: 0;
  right: 0;
  display: none;
  width: 26px;
  height: 26px;
  border-radius: 0 5px 0 5px;
  background: rgba(0,0,0,0.4);
}

.media-delete:before,
.media-delete:after {
  content: "";
  position: absolute;
  width: 2px;
  height: 16px;
  margin: -7px 0 0 0;
  background: #fff;
}

.media-delete:before {
  transform: rotate(45deg);
}

.media-delete:after {
  transform: rotate(-45deg);
}

.media-error {
  display: none;
  margin: 0 0 10px 0;
  text-align: center;
  font-size: 1.2rem;
}

.media-canvas {
  display: none;
}

もう、この時点で、labelをクリックしたらファイルを選択できるようになっています。
input type=”file”は非常に便利ですね。
ただこれだけだと、選んだファイルをそのまま送信するだけになってしまいますね。

普通にファイルを選んだだけだとプレビューもされないので、プレビューしつつ送信する画像のデータを用意するところまで、JavaScript(とjQuery)でやっちゃいましょう。

全体のコードはこうなっています。

  //写真選択
  $('.media-input').on('change',function(e){
    
    var blobURLref;
    
    $('.media-error').css('display','none');
  
    files = e.target.files;
    if(files.length === 0){
      return;
    }
    file = files[0];

    var reader = new FileReader();
  
    reader.onload = function() {
    
      var data = reader.result.indexOf('base64,') + 7;
      var mime = reader.result.substr(data, 3);
      //gif: R0l
      //jpeg: /9j
      //png: iVB
      
      // Blob URL Scheme
      if(window.URL) {
        blobURLref = window.URL.createObjectURL(file);
      } else if(window.webkitURL){
        blobURLref = window.webkitURL.createObjectURL(file);
      } else {
        return;
      }
      
      //画像の場合
      if (mime === 'R0l' || mime === '/9j' || mime === 'iVB') {
    
        $('.media-delete').css('display','block');
        
        
        var canvas = document.getElementsByClassName('media-canvas');
        var ctx = canvas.getContext('2d');
        var image = new Image();
        image.onload = function() {
          if(this.naturalWidth>=this.naturalHeight){
            image.width = image.width*(200/this.naturalHeight);
            image.height = 200;
          }
          else{
            image.height = image.height*(200/this.naturalWidth);
            image.width = 200;
          }
          canvas.width = image.width;
          canvas.height = image.height;
          ctx.drawImage(image,0,0,image.width,image.height);
          try {
            var src = canvas.toDataURL();
            $('.media-label').css('background-image','url('+ src + ')');
          } catch(e) {
            console.log('未対応');
          }        
        };
        image.src = blobURLref;

      //画像でない場合
      } else {
        files[0] = null;
        $('.media-error').css('display','block');
      }
      
    };
    reader.readAsDataURL(file);
  });
  
  //写真選択解除
  $('.media-delete').on('click',function(){
    file = null;
    $('.media-input').val('');
    $('.media-base64').val('/images/account/default.png');
    $('.media-label').css('background-image','url(/images/account/default.png)');
    $('.media-delete').css('display','none');
    $('.media-error').css('display','none');
  });

何をやっているのか、上から順番に見ていきましょう。

FileAPIでblobを取得する

特別なことをしなくてもtype=”file”でファイルを選択することができました。
このときローカルにあるファイルのデータを見ていろいろすることができます。
まずはファイルが選択されたらchangeイベントで処理を始めます。

このとき、function()の引数を使って、ターゲットとなるファイルを特定しておきます。
今回の場合multiple(複数ファイル選択)ではないので、選択された画像があるかないか?の判定をして、あれば1つめのファイルを変数fileに格納しています。

  $('.media-input').on('change',function(e){

    files = e.target.files;
    if(files.length === 0){
      return;
    }
    file = files[0];

    var reader = new FileReader();
  
    reader.onload = function() {
    //ここに処理
    };

   reader.readAsDataURL(file);

  });

わりとハマりやすいポイントらしいのですが、画像の読み込みが終わってないうちに画像のデータ(幅とか高さその他諸々)を取得しようとしても無理です。
そこでonloadイベントを使って、画像が読み込めてから処理をするというのが通例のようです。

さて画像が読み込めたら、reader.resultで画像のデータを見ることができます。
base64のコードはファイルによって微妙に異なるので、まず「base64,」という文字列を見つけて、その次の文字列を確認しています。
ここの文字列で画像形式が判別できます。

   reader.onload = function() {
    
      var data = reader.result.indexOf('base64,') + 7;
      var mime = reader.result.substr(data, 3);
      //gif: R0l
      //jpeg: /9j
      //png: iVB

そして、BlobURLを取得します。
これは都度ファイルのURLを生成するもので、これを利用するとユーザーのファイルの置き場所などがわからないので、セキュリティ的にも良いようですね。

      // Blob URL Scheme
      if(window.URL) {
        blobURLref = window.URL.createObjectURL(file);
      } else if(window.webkitURL){
        blobURLref = window.webkitURL.createObjectURL(file);
      } else {
        return;
      }

FileAPIとBlobURLについて詳しいことはこちらの記事を参照してください。

先ほど判定したMIMEタイプによって、JPEG、GIF、PNGであればメインの処理に入り、それ以外の謎のファイルであればエラーメッセージを表示して、選択したファイルを取り消します。
HTML側で「accept=”image/*”」と指定していても、たとえば.wmvファイルの拡張子を.pngに変えたりすることでファイルを選択することができてしまうので、MIMEタイプを必ずチェックしています。

      ////画像の場合
      if (mime === 'R0l' || mime === '/9j' || mime === 'iVB') {

用意しておいたcanvas要素に、画像を貼り付けるための準備をしておきます。

        var canvas = document.getElementByClassName('media-canvas');
        var ctx = canvas.getContext('2d');
        var image = new Image();

最後の行で、挿入するためのimageに先ほどのBlobURLを入れていますね。
これもファイルの読み込みをしたときと同じく、.onloadで画像の読み込みを待ちます。

そのあと、まずは画像の幅・高さを比較して、どちらが大きい画像なのかを調べます。
もし横長の画像なら高さを200pxとして、幅をその比率に合わせます。
縦長の画像ならその逆。

そしてcanvasのサイズをその画像のサイズに合わせ、drawImageで画像を貼り付けます。

        image.onload = function() {
          if(this.naturalWidth&gt;=this.naturalHeight){
            image.width = image.width*(200/this.naturalHeight);
            image.height = 200;
          }
          else{
            image.height = image.height*(200/this.naturalWidth);
            image.width = 200;
          }
          canvas.width = image.width;
          canvas.height = image.height;
          ctx.drawImage(image,0,0,image.width,image.height);
          try {
            var src = canvas.toDataURL();
            $('.media-label').css('background-image','url('+ src + ')');
            $('.media-base64').val(src);
          } catch(e) {
            console.log('未対応');
          }        
        };
        image.src = blobURLref;

.toDataURL()を使うことでcanvasの中身をbase64で取得することができます。
それをtype=”hidden”になっているinputのvalueに入れて、これを送信しようという魂胆ですね。

ユーザーが選んだ画像をそのままストレージに入れると重たいし、orientationがうまく取れなくて向きが変わってしまう問題などがあるので、これが一番楽ちんです。
いったんcanvasに描画しているのでorientationも関係なくなるし助かりますね。

せっかくだからドラッグ&ドロップも実装したい…