),可以選的方案兩個:
1、前端使用canvas做圖片處理;
2、服務端引用圖片處理的類庫來做圖片處理
兩個方案都是可以實現的,但在頁面上傳圖片,不需要存儲圖片處理結果且不需要兼容低瀏覽器的場景中,在前端做處理是更好的方案;若選擇服務端處理不僅會涉及到圖片的上傳和下載,還會涉及到服務端對圖片的存儲和定時清理等等一系列問題,所以本文主要介紹如何使用canvas做圖片處理–>打馬賽克,理解這種圖片處理的思路後,其他形式的圖片處理原理也是一樣的。
項目地址:https://github.com/saucxs/watermark-image 歡迎fork和star
測試工具的地址:https://www.mwcxs.top/static/testTool/image/index.html
二、圖片打碼
2.1思路分析
圖片來源主要有兩種,一種是直接使用圖片URL,另一種是直接操作圖片文件對象(File對象);但是無論哪一種方式,我們處理圖片的原理都是一樣的,主要是以下三個步驟:
1、創建Image對象對圖片進行加載;
2、加載成功後,將圖片寫進canvas畫布
3、最後在canvas畫布取出圖片的所有像素點,取出打碼位置的RGB值來畫馬賽克即可
2.2代碼實現
2.2.1創建Image對象對圖片進行加載
首先我們要對圖片來源分別進行處理,第一種使用圖片的URL,如下:
var img = new Image();
img.src = 'https://avatars1.githubusercontent.com/u/25859283?v=4'
第二種是圖片File對象,File對象我們不能直接使用,需要藉助URL.createObjectURL()方法將File對象轉換成一個可用於圖片src屬性的新URL對象,這個URL對象值存儲在程序內存中的,所以要在不需要使用這個URL對象時,需要手動調用URL.revokeObjectURL()方法來主動釋放該內存。如下
//html
<input type="file" name="" value="">
//js
var inputEle = document.querySelector("input");
inputEle.addEventListener("change", function() {
var fileObj = this.files[0]
var img = new Image();
/*
當圖片加載完成之後對象URL就不再需要了,釋放對象URL
*/
img.onload = function() {
URL.revokeObjectURL(this.url);
}
img.src = URL.createObjectURL(fileObj);
})
注: 若需要獲取圖片像素或轉成dataURL需設置Img的crossOrigin屬性來處理跨域問題,即設置img.crossOrigin = ‘’。
2.2.2圖片加載成功後,將圖片寫進canvas畫布
將一張加載成功後的圖片寫進canvas畫布非常簡單,只需要三行代碼即可
var canvas = document.querySelector("canvas");
var ctx = canvas.getContext('2d');
/*
從左上角開始在畫布上畫圖
*/
ctx.drawImage(img, 0, 0)
但以上代碼是直接在頁面的canvas元素上繪製圖像的,若繪製的操作動作需要多次或反覆進行,這樣會導致瀏覽器實時不斷渲染和繪製canvas元素所在的複合圖層,這會在一定程度上影響頁面的性能。
所以我們更好的方式是動態創建一個存儲在程序內存的canvas元素,然後在該canvas元素上進行畫圖以及馬賽克繪製等等的操作,所有操作完成後直接將結果一次性繪製到頁面上的canvas元素上,可提高繪製性能;
2.2.3在動態畫布上繪製馬賽克
在動態canvas畫布上繪製圖像完成後,我們可使用canvas上下文的getImageData()方法獲取該圖像的所有像素點,如下
var canvas = document.querySelector("canvas");
var ctx = canvas.getContext('2d');
/*
獲取圖像的所有像素點
*/
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
其中imageData.data就是圖像的所有像素點,也就是我們最熟悉的RGBA值,是一個一維數組,類型爲Uint8ClampedArray,數組內的所有數值都不會超過2的8次方,該像素點數組內四個數值(R, G, B, A)表示一個像素點,其中A只有兩個值,0和255,分別表示透明度0和1,如下
那麼我們可根據圖片的寬高得出圖像像素點一維數組的長度,即width * height * 4
,那麼下面我們即可操作圖像像素點來畫馬賽克了,假設我們每個馬賽克的大小爲10px,如下
var r, g, b;
for(let y = 0; y <= canvas.height; y += 10) {
for(let x = 0; x <= canvas.width; x += 10) {
/*
獲取具體位置上像素點的RGB值,然後在canvas上重新繪製圖片
*/
r = imageData[(y * canvas.width + x) * 4];
g = imageData[(y * canvas.width + x) * 4 + 1];
b = imageData[(y * canvas.width + x) * 4 + 2];
color = `rgb(${r}, ${g}, ${b})`;
/*
在圖像具體位置生成馬賽克
*/
ctx.fillStyle = "red"
ctx.fillRect(x, y, 10, 10)
}
}
馬賽克繪製完成後,再次調用canvas上下文的drawImage()方法將動態canvas畫到頁面上的canvas即可。
三、代碼封裝
將以上代碼用面向對象的方式封裝起來,實現在指定的區域打碼以及畫出線框,實現如下
3.1構造函數
/*
* @param target Object canvas目標元素
* @param image String|FileObject 圖片url或文件對象
* @param mosaicSize Number 馬賽克大小
* return dealImage實例對象
*/
function DealImage({target, image, mosaicSize=20}) {
this.canvas = document.querySelector(target);
this._canvas = document.createElement("canvas");
if(!this.canvas && this.canvas.getContext) return false
if(!image) throw new Error("缺少圖片url")
this.opt = {
image: image,
mosaicSize: mosaicSize,
ctx: this.canvas.getContext('2d'),
_ctx: this._canvas.getContext('2d'),
fileName: this.getFileName(image)
}
}
3.2原型對象
DealImage.prototype = {
constructor: DealImage,
draw: function(_opt) {
var img = new Image(),
self = this;
img.onload = function() {
/*
獲取圖片上的所有像素點
*/
self.getImageData(img);
/*
處理打碼
*/
if(_opt.mosaic) self.drawMosaic(_opt.mosaic)
/*
處理線框
*/
if(_opt.frame) self.drawFrame(_opt.frame)
/*
處理成功後,直接覆蓋至頁面上的目標canvas
*/
self.opt.ctx.drawImage(self._canvas, 0, 0)
/*
處理成功後,若傳進的是file對象則主動釋放內存
*/
if(self.url && typeof self.opt.image === "object") {
URL.revokeObjectURL(self.url);
}
/*
處理成功後的回調函數
*/
if(typeof _opt.callback == "function") {
let imgBase64 = self.canvas.toDataURL(_opt.type)
_opt.callback(imgBase64, self.opt.fileName)
}
}
/*
設置crossOrigin屬性解決資源跨域問題,
不然無法調用getImageData和toDataURL方法
https://www.zhangxinxu.com/wordpress/2018/02/crossorigin-canvas-getimagedata-cors/
*/
img.crossOrigin = '';
/*
判斷image類型,file對象則使用URL.createObjectURL轉換成blob對象
*/
if(typeof this.opt.image === "string"){
img.src = this.opt.image;
} else {
this.url = URL.createObjectURL(this.opt.image)
img.src = this.url;
}
},
drawMosaic(_opt) {
if(!this.isJson(_opt.position)) throw new TypeError("參數必須是json數組對象")
var r, g, b, color, self = this;
_opt.position.forEach(function(item, index) {
if(!self.isObject(item)) return false
for(let y = item.start[1]; y <= item.end[1]; y += self.opt.mosaicSize) {
for(let x = item.start[0]; x <= item.end[0]; x += self.opt.mosaicSize) {
/*
獲取具體位置上像素點的RGB值,然後在canvas上重新繪製圖片
*/
r = self.imageData[(y * self._canvas.width + x) * 4];
g = self.imageData[(y * self._canvas.width + x) * 4 + 1];
b = self.imageData[(y * self._canvas.width + x) * 4 + 2];
color = `rgb(${r}, ${g}, ${b})`;
/*
在圖像具體位置生成馬賽克
*/
self.opt._ctx.fillStyle = color
self.opt._ctx.fillRect(x, y, self.opt.mosaicSize, self.opt.mosaicSize)
}
}
})
},
drawFrame: function(_opt) {
if(!this.isJson(_opt.position)) throw new TypeError("參數必須是json數組對象")
var self = this;
_opt.position.forEach(function(item, index) {
if(!self.isObject(item)) return false
/*
起始一條路徑,或重置當前路徑
*/
self.opt._ctx.beginPath();
/*
把路徑移動到畫布中的指定點,不創建線條
*/
self.opt._ctx.moveTo(item.start[0], item.start[1])
/*
添加一個新點,然後在畫布中創建從該點到最後指定點的線條
*/
self.opt._ctx.lineTo(item.start[0], item.end[1])
self.opt._ctx.lineTo(item.end[1], item.end[1])
self.opt._ctx.lineTo(item.end[0], item.start[1])
self.opt._ctx.lineTo(item.start[0], item.start[1])
/*
繪製已定義的路徑
*/
self.opt._ctx.strokeStyle = _opt.color;
self.opt._ctx.stroke();
})
},
isObject: function(obj) {
return Object.prototype.toString.call(obj) === "[object Object]";
},
isJson: function(option) {
if(!(option instanceof Array)) return false
var self = this, temp = [];
option.forEach((item, index) => {
temp.push(self.isObject(item))
})
return temp.length > 0 && !temp.includes(false)
},
getFileName: function(image) {
let filename;
if(typeof image == "string") {
let tempArr = image.split("/");
filename = tempArr[tempArr.length - 1].split(".")[0];
} else {
filename = image.name.split(".")[0]
}
return filename;
},
getImageData: function(img) {
this.canvas.width = img.width;
this.canvas.height = img.height;
this._canvas.width = img.width;
this._canvas.height = img.height;
this.opt._ctx.drawImage(img, 0, 0)
/*
獲取圖像像素點
*/
this.imageData = this.opt._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height).data
}
}
3.3使用
//html
<canvas id="canvas"></canvas>
<p><img src="" alt=""></p>
<input type="file" name="" value="">
//js
var inputEle = document.querySelector("input");
inputEle.addEventListener("change", function() {
var img = new DealImage({
target: "#canvas",
mosaicSize: 10,
image: this.files[0]
})
img.draw({
type: "image/png",
mosaic: {
position: [
{start: [50, 50], end: [150, 150]},
{start: [200, 200], end: [300, 300]},
],
},
frame: {
position: [
{start: [50, 50], end: [150, 150]},
{start: [200, 200], end: [300, 300]},
],
color: "red"
},
callback: function(imgBase64, filename) {
let imgEle = document.querySelector("img"),
linkEle = document.createElement("a");
imgEle.src = imgBase64;
/*
下載圖片
*/
// linkEle.style.display = "none";
// linkEle.download = filename;
// linkEle.href = imgBase64;
// document.body.appendChild(linkEle);
// linkEle.click();
// // 然後移除
// document.body.removeChild(linkEle);
}
})
})
四、總結
以上便是canvas圖片處理思路以及代碼實現,代碼實現並不複雜。
處理的原理:1、創建image對象對圖片進行加載;2、加載成功後,將圖片寫進canvas畫布中;3、最後在canvas畫布取出圖片的所有像素點,取出打碼位置的RGB值來操作其他的(比如畫馬賽克)
優化點:直接在頁面的canvas元素上繪製圖像,若繪製的操作需要多次或者反覆進行,會導致瀏覽器實時不斷的渲染和繪製canvas元素所在的複合圖層,會影響頁面性能,我們動態創建一個存儲在內存的canvas元素,然後在該canvas上進行畫圖等操作,最後將操作結果一次性的繪製到頁面的canvas元素上,提高繪製性能。
最後,給大家推薦一個前端學習進階內推交流羣685910553(前端資料分享),不管你在地球哪個方位,
不管你參加工作幾年都歡迎你的入駐!(羣內會定期免費提供一些羣主收藏的免費學習書籍資料以及整理好的面試題和答案文檔!)
如果您對這個文章有任何異議,那麼請在文章評論處寫上你的評論。
如果您覺得這個文章有意思,那麼請分享並轉發,或者也可以關注一下表示您對我們文章的認可與鼓勵。
願大家都能在編程這條路,越走越遠。