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來實現的自動下載,所以下載的時候文件類型不會自動識別。
----------------------------------------------------------------------------
從這里開始,我只會附上js代碼,html代碼里面只是一個canvas節點.
02drawTable.js
window.onload = function () { var table = document.getElementById('table'), context = table.getContext('2d'); // 繪制表格 var width = 400, height = 400; for (var i = 0; i <= width; i = i + 40) { context.moveTo(i, 1); context.lineTo(i, height); context.stroke(); } for (var i = 0; i <= height; i = i + 40) { context.moveTo(1, i); context.lineTo(width, i); context.stroke(); } };
這個代碼看起來更簡單了.
首先,我是畫橫線,注意: 沒開始畫一條橫線,就需要把將畫筆定位到起點,畫完一條線,畫筆就會定位到橫線的重點. 畫完橫線,然后話豎線.
下面是效果
在給img對象賦值了src 屬性的時候,瀏覽器會立即開始加載圖片,只有當圖片加載完畢的時候,我們才能開始繪制圖片,所以使用了img.onload = functioin(){...}; 的方式
context.drawImage()有三種方法,下面開始介紹
- drawImage(image, x, y)
其中 image
是 image 或者 canvas 對象,x
和 y 是其在目標 canvas 里的起始坐標。
這里就會按照原生圖片進行繪制,不會進行縮放或者裁剪
- drawImage(image, x, y, width, height)
這個方法多了2個參數:width
和 height,
這兩個參數用來控制 當像canvas畫入時應該縮放的大小.
由於我們在畫圖的時候,希望圖片應該按照原比例來呈現.所以就想在頁面中寫入img標簽一樣,我們通常情況下只是放入一個參數,然后使用公式計算另一個參數.
由於要求
width/height==originWidth/originHeight
故
height=width/originWidth*originHeight
使用這個公式用寬度來計算高度就可以很按照源比例繪制, 當然如果你想把臉瘦下來,那就另說了.....
- drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
drawImage
方法的第三個也是最后一個變種有8個新參數,用於控制做切片顯示的。
第一個參數和其它的是相同的,都是一個圖像或者另一個 canvas 的引用。其它8個參數最好是參照下面的圖解,前4個是定義圖像源的切片位置和大小,后4個則是定義切片的目標顯示位置和大小
02移動精靈.html
<article> <canvas height="200" width="400" id="genius"></canvas> <div> <button id="forward">forward</button> <button id="right"> right</button> <button id="back">back</button> <button id="left">left</button> </div> </article> <script src="02移動精靈.js"></script>
我放置了一個canvas畫布和4個按鈕,讓他前后左右動.
02移動精靈.js
var ctx = document.getElementById('genius').getContext('2d'); var img = new Image(); var intervalId; var draw = function (direction) { var rowIndex = direction, // 當前是第幾行的圖片 columnIndex = 0, // 當前是第幾列的圖片 frame = 6, // 一秒有幾幀 singleWidth = 40, // 每一個小圖片的寬度 singleHeight = 65; // 每一個小圖片的高度 window.clearInterval(intervalId); intervalId = setInterval(function () { // 在沒繪制一張小圖片之前,都要清空之前繪制的圖片,這樣才能顯示出動畫效果來, ctx.clearRect(10, 10, singleWidth, singleHeight); // 在大圖上剪切繪制繪制 ctx.drawImage(img, columnIndex * singleWidth, rowIndex * singleHeight, singleWidth, singleHeight, 10, 10, singleWidth, singleHeight); columnIndex++; columnIndex %= 4; }, 1000 / frame); }
這里draw函數里防止了主要的代碼,通過計時器來使圖片動起來(注意不要使用循環)
注意在每一次移動方向后都要清除計時器,
columnIndex %= 4;columnIndex++; 這兩句是常用的循環控制語句.
02移動精靈.js
onload = function () {
img.src = "./img/DMMban.png"; img.onload = function () { // 用數字表示這個精靈移動的方向. // forward: 0,left:1,right:2,back:3 draw(0); } document.querySelector('#forward').addEventListener('click', () => { draw(0); }); document.querySelector('#left').addEventListener('click', () => { draw(1); }); document.querySelector('#right').addEventListener('click', () => { draw(2); }); document.querySelector('#back').addEventListener('click', () => { draw(3); }); };
下面是效果圖.
這個精靈並不是很好看,不過因為我不是做動畫的,找不到比較漂亮的素材,將就看吧...
面向對象版本
好了,這個列子相對於第一篇文章例子要復雜一點,所以我也做了一個面向對象的版本,使用到了原型繼承和一些其他的知識點.(關於原型繼承和JavaScript面向對象在這里有介紹: JavaScript面向對象高級(一)
注意封裝對象的方式,我個人認為這樣封裝對象時非常好的. 這也是很多大牛推薦的.在我的JavaScript高級框架設計部分將會介紹jQuery對象封裝的方式.
01-移動精靈-面向對象版本.htm
<body> <article> <canvas height="200" width="400" id="genius"></canvas> <div> <button id="forward">forward</button> <button id="right"> right</button> <button id="back">back</button> <button id="left">left</button> </div> </article> <script src="01-移動精靈-面向對象版本.js"></script> </body>
01-移動精靈-面向對象版本.js
- Genius 類的封裝
var Genius = function (option) { Genius.prototype._init_(option); } Genius.prototype = { constructor: Genius, // 把對象的初始化代碼都放在這里,把它需要用到的所有變量都綁定到它的原型上. _init_: function (option) { this.img = option.img; this.rowIndex = option.rowIndex; this.columnIndex = option.columnIndex; this.frame = option.frame; this.singleWidth = option.singleWidth; this.singleHeight = option.singleHeight }, // 由於js面向對象的特點,獲取會從父對象的prorotype里面獲取,但是設置只會設置自己的 draw: function (ctx, direction) { window.clearInterval(this.intervalId); this.rowIndex = direction; // 一定要設置,因為在setInterval里面,this指的就是window變量 var that = this; this.intervalId = setInterval(function () { // 在沒繪制一張小圖片之前,都要清空之前繪制的圖片,這樣才能顯示出動畫效果來, ctx.clearRect(10, 10, that.singleWidth, that.singleHeight); // 在大圖上剪切繪制繪制 ctx.drawImage(that.img, that.columnIndex * that.singleWidth, that.rowIndex * that.singleHeight, that.singleWidth, that.singleHeight, 10, 10, that.singleWidth, that.singleHeight); that.columnIndex++; that.columnIndex %= 4; }, 1000 / that.frame); } }
window.onload = function () { var ctx = document.getElementById('genius').getContext('2d'); var oringinImg = new Image(); oringinImg.src = "./img/DMMban.png"; var genius; oringinImg.onload = function () { // 實例化構造一個對象 genius = new Genius({ img: oringinImg, rowIndex: 0, columnIndex: 0, frame: 6, singleWidth: 40, singleHeight: 65 }); // 調用Genius的prototype里面的draw方法. genius.draw(ctx, 0); } document.querySelector('#forward').addEventListener('click', () => { genius.draw(ctx, 0); }); document.querySelector('#left').addEventListener('click', () => { genius.draw(ctx, 1); }); document.querySelector('#right').addEventListener('click', () => { genius.draw(ctx, 2); }); document.querySelector('#back').addEventListener('click', () => { genius.draw(ctx, 3); }); };
這是效果圖