2014年11月27日木曜日

canvas要素を拡張してBMP形式の画像を生成する

canvas要素は通常PNG,JPEG時々WEBP形式でしか画像を出力しません.そこでこれを拡張し,BMP形式での出力が行えるように機能を拡張してみました.BMP形式はデータサイズこそ大きいものの,処理負荷が低いので何かしら使い途があるかもしれません.

更新履歴:
v0.03 ArrayBufferのtransfer設定を追加.(パフォーマンスが改善される?)
v0.04 msToBlobの使い方を間違えていた部分を修正 






下記のコードをjsファイルにコピペしてWEBページから読み込んでください.
canvas要素をサポートする環境ではHTMLCanvasElementが拡張され,画像形式に"image/bmp"を指定可能となります.

拡張されるメソッドはtoDataURLとtoBlobです.
・toBlobメソッドではWebWorkerが動作する環境ではWorkerを使ってBlobを生成します.
・WEBGLには対応していません.

動作環境

FireFox,Chrome(Opera),IE10+.
・IE9ではbase64.jsを読み込ませることで動作するようになります.
・toBlobメソッドはBlobをサポートしている環境でのみ動作します. 
・toBlobメソッドが存在しない場合は,toDataURLから無理矢理Blobを生成してtoBlobを実装します.
・とは言え,それほど動作検証はしていませんのでご注意ください.

/*
bmpcanvas.js 0.04
Extends HTMLCanvasElement to output BMP image (OS/2 type).
Copyright (c) 2014 DEFGHI1977 
 https://twitter.com/DEFGHI1977
 http://defghi1977-onblog.blogspot.jp/

FireFox,Chrome,Opera,IE11+(IE9+)
* If you want to work on IE9 you should import base64.js.
 see https://code.google.com/p/stringencoders/source/browse/trunk/javascript/base64.js?r=210
* On IE9 and old Opera toBlob does not work.

MIT License
*/
"use strict";
if(!window.HTMLCanvasElement){return;}
(function(cproto){
 //for IE9
 var Uint8Array = window.Uint8Array;
 if(!Uint8Array){
  //NOTE:my Uint8Array does'nt have buffer property.
  Uint8Array = function(length){this.length = length};
  Uint8Array.prototype = [];
  Uint8Array.prototype.set = function(arr, offset){
   for(var i = 0, len = arr.length; i<len; i++){
    this[i + offset] = arr[i] & 255;
   }
  };
 }
 //define btoa by base64.js
 var btoa = window.btoa || window.base64.encode;

 //original methods.
 var toDataURL = cproto.toDataURL;
 var toBlob = cproto.toBlob || cproto.webkitToBlob;

 //string contstants.
 var bmpRegex = /image\/(?:bmp|x-ms-bmp|x-windows-bmp)/;
 var bmpType = "image/bmp";
 var base64 = "base64,";
 var bmpScheme = "data:" + bmpType + ";" + base64;
 var message = "message";
 var nullstr = "";

 //define toBlob for chrome, ie e.t.c.
 toBlob = toBlob || function(f, type, params){
  var url = this.toDataURL(type, params);
  var b64 = url.split(base64)[1];
  var barr = new Uint8Array(atob(b64).split(nullstr).map(function(c){
   return c.charCodeAt(0);
  }));
  f(new Blob([barr], {type: type}));
 };

 //override toDataURL
 cproto.toDataURL = function(type){
  if(!bmpRegex.test(type)){
   return toDataURL.apply(this, arguments);
  }
  //see http://stackoverflow.com/questions/12710001/how-to-convert-uint8-array-to-base64-encoded-string
  var bmp = toBMP(getAll(this), this.width, this.height);
  var chars = new Array(bmp.length);
  for(var i = 0, len = bmp.length; i<len; i++){
   chars[i] = (String.fromCharCode(bmp[i]));
  }
  return bmpScheme + btoa(chars.join(nullstr));
 };

 //override toBlob
 cproto.toBlob = function(callback, type){
  if(!bmpRegex.test(type)){
   return toBlob.apply(this, arguments);
  }
  try{
   var worker = getWorker();
   worker.addEventListener(message, function(e){
    //callback
    var data = e.data.buffer ? new Uint8Array(e.data.buffer): e.data.data;
    callback(new Blob([data], {type: bmpType}));
    URL.revokeObjectURL(worker.URL);
   }, false);
   worker.onerror = function(e){console.log(e.message);}
   var data = getAll(this);
   worker.postMessage({
    data: data,
    buffer: data.buffer,
    w: this.width,
    h: this.height
   }, [data.buffer]);//transferable
  }catch(e){
   //when worker doesn't work by security error.
   var bmp = toBMP(getAll(this), this.width, this.height);
   callback(new Blob([bmp], {type: bmpType}));
  }
 };
 
 //create WebWorker
 var getWorker = (function(){
  var listener = function(e){
   try{
    var data = e.data.buffer ? new Uint8Array(e.data.buffer) : e.data.data;
    var bmp = toBMP(data, e.data.w, e.data.h);
    postMessage({data: bmp, buffer: bmp.buffer}, [bmp.buffer]);//transferable
   }finally{
    self.close();
   }
  }
  var workerSource = "'use strict';" + toBMP.toString() 
   + "self.addEventListener('message', " + listener.toString() + ", false);";
  var jsType = "text/javascript";
  return function(){
   var workerSourceBlob 
    = new Blob([workerSource], {type: jsType});
   var workerURL = URL.createObjectURL(workerSourceBlob);
   var worker = new Worker(workerURL);
   worker.URL = workerURL;
   return worker;
  };
 })();
 
 //get whole image data.
 function getAll(canvas){
  return  canvas.getContext("2d")
   .getImageData(0, 0, canvas.width, canvas.height).data;
 }

 //convert Uint8ClampledArray of canvas to Uint8Array of bmp image.
 function toBMP(data, w, h){
  //see http://ja.wikipedia.org/wiki/Windows_bitmap

  var bgrw = w * 3;//horizontal byte length
  var filler = bgrw % 4 == 0 ? 0 : 4 - bgrw % 4
  var size = (bgrw + filler) * h + 26;
  var bmp = new Uint8Array(size);

  //BITMAPFILEHEADER 14bytes
  bmp.set([
   0x42, 0x4d, //fileType
   size, size>>>8, size>>>16, size>>>24, //fileSize
   0, 0, 0, 0,//reserved
   26, 0, 0, 0 //offset
  ], 0);

  //BITMAPCOREHEADER 12bytes
  bmp.set([
   12, 0, 0, 0, //header size
   w, w>>>8, //width
   h, h>>>8, //height
   1, 0, //plane
   24, 0 //bits par pixcel
  ], 14);

  //IMAGE DATA
  var i = 26;//offset
  for(var y = 0; y < h; y++){
   for(var x = 0; x < w; x++){
    var s = (x + (h - y - 1) * w) * 4;//bottom up
    bmp[i++] = data[s + 2];//B
    bmp[i++] = data[s + 1];//G
    bmp[i++] = data[s];//R
   }
   //0 filling
   for(var j = 0; j < filler; j++){
    bmp[i++] = 0;
   }
  }
  return bmp;
 }
})(HTMLCanvasElement.prototype);

見どころ

メイン処理とWorker内部とで同じ関数を呼び出したい場合,Workerが参照するコードをFunction.toStringで取得して文字列として切り貼りし,Blobを使ってURL化するとキレイに記述できます.
ですが,この方法だとオリジンの問題からセキュリティエラーを発生する事もあります.

0 件のコメント:

コメントを投稿