モバイルブラウザでの画像アップロードについて覚え書き
Posted: Updated:
前置き
スマートフォンブラウザで画像アップロードしたいという要件があったので、あんまり無理しないで実装できるとこまでやったら、どうなるのかやってみた。
やりたいことは、アップロードに加えて、画像データにリサイズ処理を適用すること。さすがに3G回線で2MB近い画像データを、input[type="file"]でそのまま送りつけるのは無理がある。
某所で書いたブツの要約版なので、某所のほうを見た諸氏はアレでソレして解釈してください : )
サンプルコード
基本方針としては、以下のようなコードで処理することになる。
var elFileInput = document.getElementById('js-select-photo'),
    elPreview   = document.getElementById('js-preview-photo'),
    createObjectURL = window.webkitURL && window.webkitURL.createObjectURL;
if (!createObjectURL) {
  throw new Error('This browser is not supported for `createObjectURL`');
}
// ここではサンプルとして、選択された画像を0.5倍に縮小し、画質80のJPEGで書き出す
var RESIZE_RATIO = 0.5,
    JPEG_QUALITY = 0.8;
elFileInput.addEventListener('change', function(evt) {
  var file = evt.target.files[0],
      image = document.createElement('image');
  image.src = createObjectURL(file);
  image.onload = function() {
    var tmpCvs = document.createElement('canvas'),
        tmpCxt = tmpCvs.getContext('2d'),
        origSize = {
          width : image.width,
          height: image.height
        },
        targetSize = {
          width : origSize.width * RESIZE_RATIO,
          height: origSize.height * RESIZE_RATIO
        };
    tmpCvs.width = targetSize.width;
    tmpCvs.height = targetSize.height;
    tmpCxt.drawImage(image, 0, 0, origSize.width, origSize.height, 0, 0, targetSize.width, targetSize.height);
    elPreview.src = tmpCvs.toDataURL('image/jpeg', JPEG_QUALITY);
  };
}, false);
URL.createObjectURLでリソースURLを取得したら、img要素を経由してcanvas要素に落とし込むときについでにリサイズする感じ。が、まあさすがはモバイルブラウザ、ターゲット環境をある程度絞ったとしても一筋縄ではいかない。
諸処の問題
対象環境を、iOS 6以降&Android 4.0以降とした上で、下記のような問題がある。
iOS 6以降でないとtype="file"未サポート
お馴染みかつ当たり前
ので、対象環境をiOS 6以降に限定している。一応挙げた。
iOSで大きい画像のときにCanvas描画がひしゃげる
たぶん見た方が早い。Canvas要素において、ObjectURLからloadさせたimg要素をContext.drawImage()した時点で発生する。
詳しい話はiOS6でメガピクセル画像をCanvasに描画するとおかしくなってしまう件と、その対処 - snippets from shinichitomita’s journalに載っていて、しかもそれを解決しているライブラリまで用意されているので至れりつくせり。
Android 3.0 以降で対応しているはずの XHR2 でBlobを投げられない
Galaxy S3 とかでXHR2でBlob投げるのを実際に試してみたところ、どうにもこうにもBlobで投げるとリクエストボディが空のまま飛んでいってしまう。
これは、Send a JPEG Blob with AJAX on Android — Ghinda でも同様に指摘されているようなので、そういうものらしい。ブラウザ側の実装が腐っているのは諦めるしか無いので、BlobではなくData URI、ようはただの文字列としてリクエストすることにする。
Android の Canvas.toDataURLがimage/pngのみ
3に関連する話になるのだけど、Blobで投げることを諦めてData URIとして投げつけようとすると、頼みの綱のCanvas.toDataURL()が、image/jpegの指定に対応していなかったりする。
これ自体はバグではなくて、canvas.toDataURL('image/jpeg') returns image/png on Android · Issue #33 · scottjehl/Device-Bugsで同じ話が挙がっているが、とどのつまり仕様として必ずサポートすべきはimage/pngのみとされているため、しょうがないよねという話。
ただ現実的には、ファイルサイズとか踏まえてもJPEGで送りつけたいことのほうが多いので、JavaScriptでエンコードさせる。これもライブラリ頼みになってしまい恐縮だが、owencm/javascript-jpeg-encoderを拝借して下記のようにする。
var isEncoderNeeded = document.createElement('canvas').toDataURL('image/jpeg').indexOf('data:image/jpeg') !== 0,
    jpegEnc,
    jpegUri;
 
if (isEncoderNeeded) {
  jpegEnc = new JPEGEncoder();
  jpegUri = jpegEnc.encode(context.getImageData(0, 0, canvas.width, canvas.height), 0.8 * 100);
} else {
  jpegUri = canvas.toDataURL('image/jpeg', 80);
}
feature detection的だが、こんな感じで良いはず。
まとめ
以上のような問題と対処方法を踏まえて、適当に寄せ集めライブラリっぽく仕立てれば、今回の対象環境であればソコソコ使い物になるっぽい。
拡大縮小を含め、ユーザーに自由なトリムを認めるUIを用意するかどうかは、要件と工数次第といったところだろう。UIを通してパラメータさえ決めることができれば、どこでトリムしてどれくらいリサイズするか、といったことは容易に対応できるだろう。
あとCanvasから1ピクセルずつ取り出して画像フィルタ処理を行うのも、検証として軽く実装してみた。のだが、ちょっと複雑なフィルタになると途端にしんどい気配なので、WebWorkerに逃がすとか処理自体を地味に最適化するとか相応の努力が必要そう。
おしまい ヾ(:3ノシヾ)ノシ
参考
- iOS6でメガピクセル画像をCanvasに描画するとおかしくなってしまう件と、その対処 - snippets from shinichitomita’s journal
- Send a JPEG Blob with AJAX on Android — Ghinda
- canvas.toDataURL('image/jpeg') returns image/png on Android · Issue #33 · scottjehl/Device-Bugs
 
