前言
今天我們要講解下如何編寫一個圖片壓縮、方向糾正外掛,附帶著會講解下如何上傳和預覽。
為什麼重點放在圖片壓縮和方向糾正?
相信大家在做專案過程中,經常會遇到上傳圖片到後端,但是由於圖片過大,需要對圖片壓縮處理。特別在移動端,手機拍的照片普遍過於大了,我們有時候只是需要上傳一張頭像,很小就夠用了。還有在部分手機上(已知蘋果手機)拍的照片存在方向角度問題,這時就需要我們來糾正圖片角度了。
很多同學多數時候是在用別人寫好的圖片壓縮上傳外掛。針對我們的需求,這些外掛有時候不能達到我們最理想的效果,自己寫呢,又不會寫,很是頭疼。今天就深入剖析講解下,教會大家編寫自己的圖片壓縮、方向糾正外掛,以及預覽和上傳壓縮後圖片資料。
文中用到的一些H5的api和EXIF.js等知識點如果不懂的話,請先閱讀文末尾的結語中的知識點資料。
實現原理
壓縮圖片並且上傳主要用到filereader、canvas 以及 formdata 這三個h5的api和外掛EXIF.js。邏輯並不難。整個過程就是:
(1)使用者使用input file上傳圖片的時候,用filereader讀取使用者上傳的圖片資料(base64格式)
(2)把圖片資料傳入img物件,然後將img繪製到canvas上,用EXIF.js對圖片方向進行糾正,再呼叫canvas.toDataURL對圖片進行壓縮,獲取到壓縮後的base64格式圖片資料,轉成二進位制
(3)獲取到壓縮後的圖片二進位制資料,預覽。
(4)將壓縮後的圖片二進位制資料塞入formdata,再通過XmlHttpRequest提交formdata
如此四步,就完成了圖片的壓縮、方向糾正、預覽和上傳。
外掛設計思考
考慮到在實際專案中,可能用不同的開發框架(vue.js/JQ/react.js/angular.js/anu.js等),圖片預覽的UI樣式也可能不同,圖片資料上傳方法可能不同。因為圖片壓縮和方向糾正這兩塊的邏輯多變性比較低,我們這裡把圖片壓縮和方向糾正抽離出來,封裝為一個外掛庫。
【一】獲取圖片資料
先是獲取圖片資料,也就是監聽input file的change事件,然後獲取到用來壓縮上傳的檔案物件files,將files傳到【圖片壓縮、方向糾正外掛】中進行處理。
這時候根據每個人的需求,也可以預覽未壓縮的圖片。
//監聽上傳元件input的onchange事件,壓縮圖片,糾正圖片方向,同時獲取壓縮後的圖片
filechooser.onchange = function () {
var fileList = this.files;
//預覽壓縮前的圖片
var files = Array.prototype.slice.call(fileList);
files.forEach(function (file, i) {
var reader = new FileReader();
reader.onload = function () {
var li = document.createElement("li")
li.style.backgroundImage = 'url('+this.result+')';
document.querySelector('.img_list').appendChild(li)
}
reader.readAsDataURL(file);
});
//處理圖片列表,getCompressiveFileList接受處理後的圖片資料列表
//下面兩行程式碼為圖片壓縮、方向糾正外掛的用法,具體實現細節請繼續往下閱讀 ~_~ ↓↓↓
var process = window.lzImgProcess();
process(fileList, getCompressiveFileList);
}
複製程式碼
【二】圖片壓縮、方向糾正外掛實現
上面做完圖片資料的獲取後,就可以做process壓縮圖片的方法了。而壓縮圖片也並不是直接把圖片繪製到canvas再呼叫一下toDataURL就行的。
在IOS中,canvas繪製圖片是有兩個限制的:
首先是圖片的大小,如果圖片的大小超過兩百萬畫素,圖片也是無法繪製到canvas上的,呼叫drawImage的時候不會報錯,但是你用toDataURL獲取圖片資料的時候獲取到的是空的圖片資料。
再者就是canvas的大小有限制,如果canvas的大小大於大概五百萬畫素(即寬高乘積)的時候,不僅圖片畫不出來,其他什麼東西也都是畫不出來的。
應對上面兩種限制,我把圖片寬度、高度壓縮控制在1000px以內,這樣圖片最大就不超過兩百萬畫素了。在前端開發中,1000px*1000px基本可以滿足絕大部分的需求了。當然了還有更完美的瓦片式繪製的方法,我們這裡就說瓦片式繪製方法了。
如此一來就解決了IOS上的兩種限制了。
除了上面所述的限制,還有兩個坑,一個就是canvas的toDataURL是隻能壓縮jpg的(這句話的詳細解釋可以看下面的Tip講解
),當使用者上傳的圖片是png的話,就需要轉成jpg,也就是統一用canvas.toDataURL('image/jpeg', 0.5) , 型別統一設成jpeg,而壓縮比就自己控制了。
另一個就是如果是png轉jpg,繪製到canvas上的時候,canvas存在透明區域的話,當轉成jpg的時候透明區域會變成黑色,因為canvas的透明畫素預設為rgba(0,0,0,0),所以轉成jpg就變成rgba(0,0,0,1)了,也就是透明背景會變成了黑色。解決辦法就是繪製之前在canvas上鋪一層白色的底色。
在壓縮圖片之前,我們判斷圖片角度,如果圖片角度不正確,還需要用EXIF.js把圖片角度糾正過來。
壓縮完圖片,把base64的圖片資料轉成二進位制資料儲存到暫存區中,等待被getBlobList獲取使用。
Tip:
canvas的toDataURL是隻能壓縮jpg
這句話我可能說的不清楚,我想表達的意思是這個api無論是jpeg還是png,最後匯出的時候,都跟jpeg沒啥差別了。因為png圖片的透明性質在canvas中是無效的,會被canvas新增預設的黑色背景,我在文中講解的時候,用白色背景處理了,所以最後匯出的圖片,無論你設定的是png還是jpeg都跟jpeg沒啥區別了,因為無法保持png的透明度性質了
(function(window) {
/**
*
* 作者:混沌傳奇
*
* 郵箱地址:iot-pro_lizeng@foxmail.com
*
* 日期:2017-10-26
*
* 外掛功能:壓縮圖片&&糾正圖片方向&&返回二進位制(Blob)圖片後設資料組成的列表
*
*/
window.lzImgProcess = function () {
var Orientation = '', //圖片方向角
blobList = [], //壓縮後的二進位制圖片資料列表
canvas = document.createElement("canvas"), //用於壓縮圖片(糾正圖片方向)的canvas
ctx = canvas.getContext('2d'),
file_type = 'image/jpeg', //圖片型別
qlty = 0.5, //圖片壓縮品質,預設是0.5,可選範圍是0-1的數字型別的值,可配置
imgWH = 1000; //壓縮後的圖片的最大寬度和高度,預設是1000px,可配置
/**
* @actionName process,
* 方法功能:壓縮圖片&&糾正圖片方向&&返回二進位制(Blob)圖片後設資料
*
* @param fileList,傳入函式的檔案列表物件,fileList物件是來自使用者在一個<input>元素上選擇檔案後返回的FileList物件
* 注意:圖片型別必須是jpeg||png
* 比如:<input id="uploadImage" onchange="loadImageFile();" />
* function loadImageFile() {
* //獲取返回的fileList物件
* var fileList = document.getElementById("uploadImage").files;
* }
* @param getBlobList [Blob],獲取壓縮結果的鉤子函式,接受一個引數。
* 功能:在圖片壓縮完畢後,獲取壓縮後的二進位制圖片資料物件組成的陣列,引數即:壓縮後的二進位制圖片資料(blob)組成的list
*
* @param quality,傳入函式的圖片壓縮比率(品質),可選範圍0-1的數字型別的值,預設是0.5
*
* @param WH,傳入函式的圖片壓縮後的最大圖片寬度和高度,預設是1000,單位是px,可自由配置。
* 注意:最好不要超過1000,數字過大,容易導致canvas壓縮失敗。由於沒做瓦片處理,所以有這個限制。1000*1000的圖片在前端中,基本也夠用了。
*
*/
function process (fileList, getBlobList, quality, WH) {
blobList = []; //初始化blobList
// 判斷引數fileList的長度是否大於0
if (!fileList.length){
console.log('警告:傳進方法process的引數fileList長度必須大於零!!!')
return;
}
//如果quality引數有值,則把quality賦值給qlty(圖片壓縮的品質)
if(quality)
qlty = quality;
//如果WH引數有值,則把WH賦值給imgWH(壓縮後的圖片的最大寬度和高度)
if(WH&&WH<1000&&WH>0){
imgWH = WH;
}
// 把傳進來的fileList轉為陣列型別
var files = Array.prototype.slice.call(fileList);
files.forEach(function (file, i) {
if (!/\/(?:jpeg|png)/i.test(file.type)){
console.log('警告:圖片必須是jpeg||png型別!!!');
return;
}
// file_type = file.type;
var reader = new FileReader();
// 獲取圖片壓縮前大小,列印圖片壓縮前大小
var size = file.size/1024 > 1024 ? (~~(10*file.size/1024/1024))/10 + "MB" : ~~(file.size/1024) + "KB";
// console.log('size:', size)
reader.onload = function () {
var img = new Image();
img.src = this.result;
// 圖片載入完畢之後進行壓縮
if (img.complete) {
callback();
} else {
img.onload = callback;
}
function callback() {
//獲取照片方向角屬性,使用者旋轉控制
EXIF.getData(img, function() {
// alert(EXIF.pretty(this));
EXIF.getAllTags(this);
// alert(EXIF.getTag(this, 'Orientation'));
Orientation = EXIF.getTag(this, 'Orientation');
console.log('Orientation:', Orientation)
if(Orientation == ""||Orientation == undefined||Orientation == null){
Orientation = 1;
}
});
//獲取壓縮後的圖片二進位制資料
var data = GetImgCompress(img);
//將二進位制資料塞入到二進位制資料列表中
blobList.push(data);
//將壓縮後的二進位制圖片資料物件(blob)組成的list通過鉤子函式返回出去
if(blobList.length===files.length){
if(getBlobList)
getBlobList(blobList);
}
img = null;
}
};
reader.readAsDataURL(file);
})
}
/**
* @actionName GetImgCompress,
* 功能:判斷上傳圖片的方向,如果不是正確的,進行修正,並對圖片進行壓縮,壓縮完後,返回壓縮後的二進位制圖片資料
*
* @param img, 用來壓縮的圖片物件
*
* @returns 返回的壓縮後的二進位制圖片資料
*/
function GetImgCompress(img){
//如果方向角不為1,都需要進行旋轉
if(Orientation != 1){
switch(Orientation){
case 6://需要順時針90度旋轉
rotateImg(img,'right',canvas);
break;
case 8://需要逆時針90度旋轉
rotateImg(img,'left',canvas);
break;
case 3://需要180度旋轉
rotateImg(img,'right2',canvas);//轉兩次
break;
}
}else{
//不做旋轉
rotateImg(img,'no',canvas);
}
var ndata;
ndata = canvas.toDataURL(file_type, qlty);
//列印壓縮前後的大小,以及壓縮比率
// var initSize = img.src.length;
// console.log('壓縮前:' + initSize);
// console.log('壓縮後:' + ndata.length, 'base64資料', ndata);
// console.log('壓縮率:' + ~~(100 * (initSize - ndata.length) / initSize) + "%");
//將壓縮後的base64資料轉為二進位制資料
ndata = dataURItoBlob(ndata);
//清除canvas畫布的寬高
canvas.width = canvas.height = 0;
return ndata;
}
/**
* @actionName rotateImg,
* 功能:對圖片旋轉處理
*
* @param img, 用來矯正方向的圖片物件
*
* @param direction, 旋轉方向
*
* @param canvas, 用來繪製圖片的cavas畫布物件
*/
function rotateImg(img, direction,canvas) {
//最小與最大旋轉方向,圖片旋轉4次後回到原方向
var min_step = 0;
var max_step = 3;
if (img == null)return;
//img的高度和寬度不能在img元素隱藏後獲取,否則會出錯
var height = img.height;
var width = img.width;
if(width>imgWH || height>imgWH){
var ratio = ~~(height/width*10)/10;
if(width>height){
width = imgWH;
height = imgWH*ratio;
}else{
height = imgWH;
width = height/ratio;
}
img.width = width;
img.height = height;
}
var step = 2;
if (step == null) {
step = min_step;
}
if (direction == 'no'){
step = 0;
} else if (direction == 'left') {
step++;
//旋轉到原位置,即超過最大值
step > max_step && (step = min_step);
} else if (direction == 'right') {
step--;
step < min_step && (step = max_step);
} else {
//旋轉180度
}
//旋轉角度以弧度值為引數
var degree = step * 90 * Math.PI / 180;
switch (step) {
case 0:
canvas.width = width;
canvas.height = height;
// 鋪底色
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
ctx.drawImage(img, 0, 0, width, height);
break;
case 1:
canvas.width = height;
canvas.height = width;
// 鋪底色
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, height, width);
ctx.rotate(degree);
ctx.drawImage(img, 0, -height, width, height);
break;
case 2:
canvas.width = width;
canvas.height = height;
// 鋪底色
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
ctx.rotate(degree);
ctx.drawImage(img, -width, -height, width, height);
break;
case 3:
canvas.width = height;
canvas.height = width;
// 鋪底色
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, height, width);
ctx.rotate(degree);
ctx.drawImage(img, -width, 0, width, height);
break;
}
}
/**
* dataURL to blob, ref to https://gist.github.com/fupslot/5015897
* @param dataURI,圖片的base64格式資料
* @returns {Blob}
*/
function dataURItoBlob(dataURI) {
var byteString = atob(dataURI.split(',')[1]);
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], {type: mimeString});
}
/**
* 返回一個process方法
*
* process方法:用來壓縮圖片資料,在壓縮圖片的同時,預設會呼叫correctOrientation方法糾正圖片方向。
*
*/
return process;
}
})(window)
複製程式碼
Exif.js 提供了 JavaScript 讀取影像的原始資料的功能擴充套件,例如:拍照方向、相機裝置型號、拍攝時間、ISO 感光度、GPS 地理位置等資料。
Exif.js官方github 倉庫地址:github.com/exif-js/exi…
【三】獲取壓縮後的圖片二進位制資料,預覽圖片
完成圖片壓縮後,就可以獲取壓縮後的圖片二進位制資料了,把獲取到的圖片二進位制資料存起來;獲取到資料後,可以拿來預覽。
由於實際專案中,每個專案的UI樣式設計可能不一樣,開發者可以根據自己的UI樣式來預覽圖片。
//獲取壓縮後的圖片
function getCompressiveFileList(fileList) {
blobFileList = fileList;
// console.log('fileBlobList:', fileList);
fileList.forEach(function (blob) {
var reader = new FileReader();
reader.onload = function () {
var li = document.createElement("LI")
li.style.backgroundImage = 'url('+this.result+')';
document.querySelector('.imgCompress_list').appendChild(li)
}
reader.readAsDataURL(blob);
})
}
複製程式碼
【四】提交圖片資料到後臺
new一個formdata物件,將上一步獲取到的 blobFileList 圖片二進位制資料 append 到 formdata中,用任意你喜歡的ajax庫進行上傳。當然也可以用原生ajax上傳。
//將壓縮後的二進位制圖片資料流append到formdata物件中上傳到後臺伺服器
//注意:上傳的是formdata物件,後臺介面接收的時候,也要從formdata物件中讀取二進位制資料流
function formUpData(blobFiles){
var formData = new FormData();
formData.append("files", blobFiles);
var xhr = new XMLHttpRequest();
//連結你自己上傳圖片介面即可,這裡的介面地址,是我寫的示例,不可真實使用,講解意義更大
xhr.open('post', 'http://xxx/welcome/index/');
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log('上傳成功!');
}
};
xhr.send(formData);
}
複製程式碼
結語
完整程式碼以及demo:git倉庫地址
文中用到了一些H5的知識點,不懂的童鞋可以進入下面的知識點資料地址,詳細閱讀
HTMLCanvasElement.toDataURL()
CanvasRenderingContext2D.drawImage()
FileReader()
FormData
Blob()
EXIF.js中文部落格文章