LoginSignup
20
18

More than 5 years have passed since last update.

File APIで画像処理を行う際の注意点のまとめ

Last updated at Posted at 2018-07-24

File APIを用いると、画像データを読み込んでcanvas上に表示したり、それを加工して出力したりできる。
しかし、実装するには読み込む画像のフォーマットや環境ごとの差など、いくつか考慮しなければいけない点があったので、ここにまとめておく。

ファイルフォーマットの正確な判定

File APIで読み込んだファイルのフォーマットはFile.typeプロパティで確認できる。
しかし、このtypeプロパティの値は拡張子に応じたMIMEタイプを返しているだけなので、仮に拡張子を偽装したファイルの場合、本当のファイルフォーマットとは異なるため、続く処理でエラーが出てしまう場合がある。
そのため、本当のファイルフォーマットを判定するにはマジックナンバーを用いる。

フォーマット識別子としてのマジックナンバーとは、ファイルの種類を識別するのに使われるファイル本文中の(内容中の)特定の位置にある特定の数値のことである。ファイルの種類を識別する方法としてはファイルの拡張子や属性値(プロパティ)を使う場合もあるが、マジックナンバーとはそれらのことではなく、ファイルの本文中に表れる特定の数値のことである。ほとんどの場合、マジックナンバーはファイルの先頭に位置し、数バイト程度である。

マジックナンバー (フォーマット識別子)より引用

今回はFile APIで画像ファイルを扱うため、一般的なWeb画像形式を判定できるようにする。

フォーマット バイナリ
png 89 50 4E 47
gif 47 49 46 38
bmp 42 4D
jpg, jpeg FF D8 FF

参考:List of file signatures

function getImageFormat(arrayBuffer) {
  var arr = new Uint8Array(arrayBuffer).subarray(0, 4);
  var header = '';

  for(var i = 0; i < arr.length; i++) {
    header += arr[i].toString(16);
  }

  switch(true) {
    case /^89504e47/.test(header):
      return 'image/png';
    case /^47494638/.test(header):
      return 'image/gif';
    case /^424d/.test(header):
      return 'image/bmp';
    case /^ffd8ff/.test(header):
      return 'image/jpeg';
    default:
      return 'unknown image type';
  }
}

See the Pen get image format by magic number by Haruumi Kondo (@hal9188) on CodePen.

jpegの向き

スマートフォンやデジカメで撮影した写真をFile APIで読み込んでcanvasに表示すると、撮影した機器で見る画像の向きとcanvasに描画された画像の向きが異なる場合がある。
これはjpegのEXIF情報が持つ撮影方向(Orientation)が反映されていないため。

Orientation値 補正前の画像の向き
1 そのまま
2 左右反転
3 180度回転
4 上下反転
5 上下反転かつ時計回りに90度回転
6 時計回りに90度回転
7 上下反転かつ時計回りに270度回転
8 時計回りに270度回転

そのため画像を正しい向きに表示するには、jpegからOrientation値を取得し、それに応じてcanvasに描画する際に向きを補正してやる必要がある。

向きの取得

function getOrientation(buffer) {
  var view = new DataView(buffer);
  if (view.getUint16(0, false) !== 0xFFD8) return -2;
  var length = view.byteLength, offset = 2;
  while (offset < length) {
    var marker = view.getUint16(offset, false);
    offset += 2;
    if (marker === 0xFFE1) {
      if (view.getUint32(offset += 2, false) !== 0x45786966) return -1;
      var little = view.getUint16(offset += 6, false) === 0x4949;
      offset += view.getUint32(offset + 4, little);
      var tags = view.getUint16(offset, little);
      offset += 2;
      for (var i = 0; i < tags; i++) {
        if (view.getUint16(offset + (i * 12), little) === 0x0112) return view.getUint16(offset + (i * 12) + 8, little);
      }
    } else if((marker & 0xFF00) !== 0xFF00) {
      break;
    } else {
      offset += view.getUint16(offset, false);
    }
  }
  return -1;
}

向きを補正して描画

function drawAdjustImage(cnvs, img, orientation) {
  var ctx = cnvs.getContext('2d');
  var dx = 0;
  var dy = 0;
  var dw;
  var dh;
  var deg = 0;
  var vt = 1;
  var hr = 1;
  var rad;
  var sin;
  var cos;

  cnvs.width = (orientation >= 5) ? img.height: img.width;
  cnvs.height = (orientation >= 5) ? img.width: img.height;

  switch(orientation) {
    case 2: // flip horizontal
      hr = -1;
      dx = cnvs.width;
      break;
    case 3: // rotate 180 degrees
      deg = 180;
      dx = cnvs.width;
      dy = cnvs.height;
      break;
    case 4: // flip upside down
      vt = -1;
      dy = cnvs.height;
      break;
    case 5: // flip upside down and rotate 90 degrees clock wise
      vt = -1;
      deg = 90;
      break;
    case 6: // rotate 90 degrees clock wise
      deg = 90;
      dx = cnvs.width;
      break;
    case 7: // flip upside down and rotate 270 degrees clock wise
      vt = -1;
      deg = 270;
      dx = cnvs.width;
      dy = cnvs.height;
      break;
    case 8: // rotate 270 degrees clock wise
      deg = 270;
      dy = cnvs.height;
      break;
  }
  rad = deg * (Math.PI / 180);
  sin = Math.sin(rad);
  cos = Math.cos(rad);
  ctx.setTransform(cos * hr, sin * hr, -sin * vt, cos * vt, dx, dy);

  dw = (orientation >= 5) ? cnvs.height : cnvs.width;
  dh = (orientation >= 5) ? cnvs.width : cnvs.height;
  ctx.drawImage(img, 0, 0, dw, dh);
}

検証にはUIImageOrientation / EXIF orientation sample images - Matt Gallowayにある画像を使うと便利。

See the Pen get EXIF orientation by Haruumi Kondo (@hal9188) on CodePen.

canvasに描画可能なピクセル寸法の上限

canvasには描画できるピクセル寸法の上限が環境によってある様子。
調べた限り、下記の環境ではcanvasの幅/高さを制限しておく必要がある。

ピクセル寸法上限 環境
2048 x 2048 IE
2560 x 2560 iOS 8.0 or 8.1
iPod touch 5(iOS 8+)
4096 x 4096 iOS

参考:

toBlobメソッドの実装差異

canvasには画像を含んだBlobオブジェクトを生成するのにtoBlobメソッドが標準で用意されているが、サポートしていないブラウザや独自のメソッドを用いてるブラウザもあるため、その差異を吸収できるように実装する必要がある。

function toBlob(callback, cnvs, type, quality) {
  if(cnvs.toBlob) {
    cnvs.toBlob(callback, type, quality);
  } else if(cnvs.msToBlob) { // IE10~, EDGE
    callback(cnvs.msToBlob());
  } else { // toBlobが無い場合はpolyfillによって実行
    var binStr = atob(cnvs.toDataURL(type, quality).split(',')[1]),
      len = binStr.length,
      arr = new Uint8Array(len),
      blob;
    for(var i = 0; i < len; i++) {
      arr[i] = binStr.charCodeAt(i);
    }

    try {
      blob = new Blob([arr.buffer], {type : type || 'image/png'});
      callback(blob);
    } catch(e) {
      try {
        blob = new (window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder)();
        blob.append(arr.buffer);
        callback(blob.getBlob());
      } catch(e) {
        alert(e);
      }
    }
  }
}

Android < 4.4でBlobに画像を積んで送信できないバグ

Blobオブジェクトを用いると画像データをバイナリ形式のまま送れるが、Android 4.0.x、4.1.xではBlobを非同期通信で送るとbodyが空になってしまうバグが存在する。

XHR2 allows us to send additional formats with AJAX beside FormData, like Blob or ArrayBuffer. We can simply use the send method with our Blob as the parameter.

While the request works as expected on supporting desktop browsers, there seem> s to be a bug in Android that sends the request completely empty. Here’s a test for xhr.send(blob) using GitHub’s API, which sends both a Blob and an ArrayBuffer.

Send a JPEG Blob with AJAX on Androidより引用

Trying to send a Blob with XHR2 sends the request with an empty body on Android 4+では4.0.x、4.1.xで再現すると言っているが、実機で検証したところ4.3でも再現した。OSをアップデートしてもバグが直らない場合があるのかも知れない。
全ての実機検証は難しいので、4.1~4.3(Jelly Bean)と4.4(KitKat)で大きく変わっていることを踏まえ、とりあえずAndroid 4.4未満で再現しうると考えた方が良さそう。

これを回避するには画像データをBlobオブジェクト以外の形式にして送れば良い。
canvas.toDataURLメソッドを使えばbase64形式で画像データを出力できる。

20
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
18