叶落为重生每片落下的叶子都是为了下一次的涅槃...^_^

2012年1月14日 #

canvas保存为data:image扩展功能的实现

【已知】
canvas提供了toDataURL的接口,可以方便的将canvas画布转化成base64编码的image。目前支持的最好的是png格式,jpeg格式的现代浏览器基本也支持,但是支持的不是很好。

【想要的】
往往这么简单直接的接口通常都满足不了需求。我想要的不仅是简单的通过画布生成一个png,我不想新开一个tab,然后还要右键另存为...

我还需要更方便的自由的配置生成的图片的大小,比例等。

另外如果我还要别的图片格式,比如位图bmp,gif等怎么办...

【解决办法】
a)想直接把图片生成后download到本地,其实办法也很简单。直接改图片的mimeType,强制改成steam流类型的。比如‘image/octet-stream’,浏览器就会自动帮我们另存为.. 

b)图片大小,及比例的可控倒也好办,我们新建一个我们想要大小的canvas,把之前的canvas画布重新按照所要的比例,及大小draw到新的canvas上,然后用新的canvas来toDataURL即可。

c)想要bmp位图会麻烦些... 没有直接的接口,需要我们自己来生成。生成图片的响应头和响应体有一定的规则,略显麻烦。不过还能接受。剩下的就是性能问题,按像素级别来操作,对于一个大图来说计算量很有压力。

【实现】

/**
* covert canvas to image
* and save the image file
*/

var Canvas2Image = function () {

// check if support sth.
var $support = function () {
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d');

return {
canvas: !!ctx,
imageData: !!ctx.getImageData,
dataURL: !!canvas.toDataURL,
btoa: !!window.btoa
};
}();

var downloadMime = 'image/octet-stream';

function scaleCanvas (canvas, width, height) {
var w = canvas.width,
h = canvas.height;
if (width == undefined) {
width = w;
}
if (height == undefined) {
height = h;
}

var retCanvas = document.createElement('canvas');
var retCtx = retCanvas.getContext('2d');
retCanvas.width = width;
retCanvas.height = height;
retCtx.drawImage(canvas, 0, 0, w, h, 0, 0, width, height);
return retCanvas;
}

function getDataURL (canvas, type, width, height) {
canvas = scaleCanvas(canvas, width, height);
return canvas.toDataURL(type);
}

function saveFile (strData) {
document.location.href = strData;
}

function genImage(strData) {
var img = document.createElement('img');
img.src = strData;
return img;
}
function fixType (type) {
type = type.toLowerCase().replace(/jpg/i, 'jpeg');
var r = type.match(/png|jpeg|bmp|gif/)[0];
return 'image/' + r;
}
function encodeData (data) {
if (!window.btoa) { throw 'btoa undefined' }
var str = '';
if (typeof data == 'string') {
str = data;
} else {
for (var i = 0; i < data.length; i ++) {
str += String.fromCharCode(data[i]);
}
}

return btoa(str);
}
function getImageData (canvas) {
var w = canvas.width,
h = canvas.height;
return canvas.getContext('2d').getImageData(0, 0, w, h);
}
function makeURI (strData, type) {
return 'data:' + type + ';base64,' + strData;
}


/**
* create bitmap image
* 按照规则生成图片响应头和响应体
*/
var genBitmapImage = function (data) {
var imgHeader = [],
imgInfoHeader = [];

var width = data.width,
height = data.height;

imgHeader.push(0x42); // 66 -> B
imgHeader.push(0x4d); // 77 -> M

var fsize = width * height * 3 + 54; // header size:54 bytes
imgHeader.push(fsize % 256); // r
fsize = Math.floor(fsize / 256);
imgHeader.push(fsize % 256); // g
fsize = Math.floor(fsize / 256);
imgHeader.push(fsize % 256); // b
fsize = Math.floor(fsize / 256);
imgHeader.push(fsize % 256); // a

imgHeader.push(0);
imgHeader.push(0);
imgHeader.push(0);
imgHeader.push(0);

imgHeader.push(54); // offset -> 6
imgHeader.push(0);
imgHeader.push(0);
imgHeader.push(0);

// info header
imgInfoHeader.push(40); // info header size
imgInfoHeader.push(0);
imgInfoHeader.push(0);
imgInfoHeader.push(0);

// 横向info
var _width = width;
imgInfoHeader.push(_width % 256);
_width = Math.floor(_width / 256);
imgInfoHeader.push(_width % 256);
_width = Math.floor(_width / 256);
imgInfoHeader.push(_width % 256);
_width = Math.floor(_width / 256);
imgInfoHeader.push(_width % 256);

// 纵向info
var _height = height;
imgInfoHeader.push(_height % 256);
_height = Math.floor(_height / 256);
imgInfoHeader.push(_height % 256);
_height = Math.floor(_height / 256);
imgInfoHeader.push(_height % 256);
_height = Math.floor(_height / 256);
imgInfoHeader.push(_height % 256);

imgInfoHeader.push(1);
imgInfoHeader.push(0);
imgInfoHeader.push(24); // 24位bitmap
imgInfoHeader.push(0);

// no compression
imgInfoHeader.push(0);
imgInfoHeader.push(0);
imgInfoHeader.push(0);
imgInfoHeader.push(0);

// pixel data
var dataSize = width * height * 3;
imgInfoHeader.push(dataSize % 256);
dataSize = Math.floor(dataSize / 256);
imgInfoHeader.push(dataSize % 256);
dataSize = Math.floor(dataSize / 256);
imgInfoHeader.push(dataSize % 256);
dataSize = Math.floor(dataSize / 256);
imgInfoHeader.push(dataSize % 256);

// blank space
for (var i = 0; i < 16; i ++) {
imgInfoHeader.push(0);
}

var padding = (4 - ((width * 3) % 4)) % 4;
var imgData = data.data;
var strPixelData = '';
var y = height;
do {
var offsetY = width * (y - 1) * 4;
var strPixelRow = '';
for (var x = 0; x < width; x ++) {
var offsetX = 4 * x;
strPixelRow += String.fromCharCode(imgData[offsetY + offsetX + 2]);
strPixelRow += String.fromCharCode(imgData[offsetY + offsetX + 1]);
strPixelRow += String.fromCharCode(imgData[offsetY + offsetX]);
}
for (var n = 0; n < padding; n ++) {
strPixelRow += String.fromCharCode(0);
}

strPixelData += strPixelRow;
} while(-- y);

return (encodeData(imgHeader.concat(imgInfoHeader)) + encodeData(strPixelData));

};

/**
* saveAsImage
* @param canvasElement
* @param {String} image type
* @param {Number} [optional] png width
* @param {Number} [optional] png height
*/
var saveAsImage = function (canvas, width, height, type) {
if ($support.canvas && $support.dataURL) {
if (type == undefined) { type = 'png'; }
type = fixType(type);
if (/bmp/.test(type)) {
var data = getImageData(scaleCanvas(canvas, width, height));
var strData = genBitmapImage(data);
saveFile(makeURI(strData, downloadMime));
} else {
var strData = getDataURL(canvas, type, width, height);
saveFile(strData.replace(type, downloadMime));
}

}
}

var convertToImage = function (canvas, width, height, type) {
if ($support.canvas && $support.dataURL) {
if (type == undefined) { type = 'png'; }
type = fixType(type);

if (/bmp/.test(type)) {
var data = getImageData(scaleCanvas(canvas, width, height));
var strData = genBitmapImage(data);
return genImage(makeURI(strData, 'image/bmp'));
} else {
var strData = getDataURL(canvas, type, width, height);
return genImage(strData);
}
}
}



return {
saveAsImage: saveAsImage,
saveAsPNG: function (canvas, width, height) {
return saveAsImage(canvas, width, height, 'png');
},
saveAsJPEG: function (canvas, width, height) {
return saveAsImage(canvas, width, height, 'jpeg');
},
saveAsGIF: function (canvas, width, height) {
return saveAsImage(canvas, width, height, 'gif')
},
saveAsBMP: function (canvas, width, height) {
return saveAsImage(canvas, width, height, 'bmp');
},

convertToImage: convertToImage,
convertToPNG: function (canvas, width, height) {
return convertToImage(canvas, width, height, 'png');
},
convertToJPEG: function (canvas, width, height) {
return convertToImage(canvas, width, height, 'jpeg');
},
convertToGIF: function (canvas, width, height) {
return convertToImage(canvas, width, height, 'gif');
},
convertToBMP: function (canvas, width, height) {
return convertToImage(canvas, width, height, 'bmp');
}
};

}();

【Demo】
http://hongru.github.com/proj/canvas2image/index.html
可以试着在canvas上涂涂画画,然后保存看看。如果用bmp格式的话,需要支持 btoa 的base64编码,关于base64编码规则可看上一篇博文

【不完美的地方】
1)jpeg接口本身就不完善,当canvas没有填充颜色或图片时,保存的jpeg由于是直接由png的alpha通道强制转换过来的,所以在png的透明部分在jpeg里面就是黑色的。

2)gif的限制太多。且可用性不大,有png就够了

3)bmp位图生成,计算量稍显大了。

4)由于是强制改mimeType来实现的自动下载,所以下载的时候文件类型不会自动识别。
 

posted @ 2012-01-14 20:27 岑安 阅读(1587) 评论(1) 编辑

关于base64编码的原理及实现

我们的图片大部分都是可以转换成base64编码的data:image。 这个在将canvas保存为img的时候尤其有用。虽然除ie外,大部分现代浏览器都已经支持原生的基于base64的encode和decode,例如btoa和atob。(将canvas画布保存成img并强制改变mimetype进行下载,会在下一篇记录)

但是处于好奇心,还是驱使我去了解下base64编码的原理。以便也在不支持原生base64编码的ie下可以得以实现。

【Base64】
-base64的编码都是按字符串长度,以每3个8bit的字符为一组,
-然后针对每组,首先获取每个字符的ASCII编码,
-然后将ASCII编码转换成8bit的二进制,得到一组3*8=24bit的字节
-然后再将这24bit划分为4个6bit的字节,并在每个6bit的字节前面都填两个高位0,得到4个8bit的字节
-然后将这4个8bit的字节转换成10进制,对照Base64编码表 (下表),得到对应编码后的字符。

(注:1. 要求被编码字符是8bit的,所以须在ASCII编码范围内,\u0000-\u00ff,中文就不行。
   2. 如果被编码字符长度不是3的倍数的时候,则都用0代替,对应的输出字符为=)

Base64 编码表
ValueChar   ValueChar   ValueChar   ValueChar
0 A 16 Q 32 g 48 w
1 B 17 R 33 h 49 x
2 C 18 S 34 i 50 y
3 D 19 T 35 j 51 z
4 E 20 U 36 k 52 0
5 F 21 V 37 l 53 1
6 G 22 W 38 m 54 2
7 H 23 X 39 n 55 3
8 I 24 Y 40 o 56 4
9 J 25 Z 41 p 57 5
10 K 26 a 42 q 58 6
11 L 27 b 43 r 59 7
12 M 28 c 44 s 60 8
13 N 29 d 45 t 61 9
14 O 30 e 46 u 62 +
15 P 31 f 47 v 63 /

比如举下面2个例子:
a) 字符长度为能被3整除时:比如“Tom” :

            T           o           m
ASCII: 84 111 109
8bit字节: 01010100 01101111 01101101
6bit字节: 010101 000110 111101 101101
十进制: 21 6 61 45
对应编码: V G 9 t

所以,btoa('Tom') = VG9t

b) 字符串长度不能被3整除时,比如“Lucy”:

            L           u           c           y
ASCII: 76 117 99 121
8bit字节: 01001100 01110101 01100011 01111001 00000000 00000000
6bit字节: 010011 000111 010101 100011 011110 010000 000000 000000
十进制: 19 7 21 35 30 16 (异常) (异常)
对应编码: T H V j e Q = =

由于Lucy只有4个字母,所以按3个一组的话,第二组还有两个空位,所以需要用0来补齐。这里就需要注意,因为是需要补齐而出现的0,所以转化成十进制的时候就不能按常规用base64编码表来对应,所以不是a, 可以理解成为一种特殊的“异常”,编码应该对应“=”。

有了上面的理论,那我们实现一个base64编码就容易了。
 

/**
* base64 encoding & decoding
* for fixing browsers which don't support Base64 | btoa |atob
*/

(function (win, undefined) {

var Base64 = function () {
var base64hash = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

// btoa method
function _btoa (s) {
if (/([^\u0000-\u00ff])/.test(s)) {
throw new Error('INVALID_CHARACTER_ERR');
}
var i = 0,
prev,
ascii,
mod,
result = [];

while (i < s.length) {
ascii = s.charCodeAt(i);
mod = i % 3;

switch(mod) {
// 第一个6位只需要让8位二进制右移两位
case 0:
result.push(base64hash.charAt(ascii >> 2));
break;
//第二个6位 = 第一个8位的后两位 + 第二个8位的前4位
case 1:
result.push(base64hash.charAt((prev & 3) << 4 | (ascii >> 4)));
break;
//第三个6位 = 第二个8位的后4位 + 第三个8位的前2位
//第4个6位 = 第三个8位的后6位
case 2:
result.push(base64hash.charAt((prev & 0x0f) << 2 | (ascii >> 6)));
result.push(base64hash.charAt(ascii & 0x3f));
break;
}

prev = ascii;
i ++;
}

// 循环结束后看mod, 为0 证明需补3个6位,第一个为最后一个8位的最后两位后面补4个0。另外两个6位对应的是异常的“=”;
// mod为1,证明还需补两个6位,一个是最后一个8位的后4位补两个0,另一个对应异常的“=”
if(mod == 0) {
result.push(base64hash.charAt((prev & 3) << 4));
result.push('==');
} else if (mod == 1) {
result.push(base64hash.charAt((prev & 0x0f) << 2));
result.push('=');
}

return result.join('');
}

// atob method
// 逆转encode的思路即可
function _atob (s) {
s = s.replace(/\s|=/g, '');
var cur,
prev,
mod,
i = 0,
result = [];

while (i < s.length) {
cur = base64hash.indexOf(s.charAt(i));
mod = i % 4;

switch (mod) {
case 0:
//TODO
break;
case 1:
result.push(String.fromCharCode(prev << 2 | cur >> 4));
break;
case 2:
result.push(String.fromCharCode((prev & 0x0f) << 4 | cur >> 2));
break;
case 3:
result.push(String.fromCharCode((prev & 3) << 6 | cur));
break;

}

prev = cur;
i ++;
}

return result.join('');
}

return {
btoa: _btoa,
atob: _atob,
encode: _btoa,
decode: _atob
};
}();

if (!win.Base64) { win.Base64 = Base64 }
if (!win.btoa) { win.btoa = Base64.btoa }
if (!win.atob) { win.atob = Base64.atob }

})(window)

 

Base64 example

 

posted @ 2012-01-14 11:47 岑安 阅读(2617) 评论(7) 编辑

导航

统计信息

News