今天咱們要講解下如何編寫一個圖片壓縮、方向糾正插件,附帶着會講解下如何上傳和預覽。html
爲何重點放在圖片壓縮和方向糾正?前端
相信你們在作項目過程當中,常常會遇到上傳圖片到後端,可是因爲圖片過大,須要對圖片壓縮處理。特別在移動端,手機拍的照片廣泛過於大了,咱們有時候只是須要上傳一張頭像,很小就夠用了。還有在部分手機上(已知蘋果手機)拍的照片存在方向角度問題,這時就須要咱們來糾正圖片角度了。vue
不少同窗多數時候是在用別人寫好的圖片壓縮上傳插件。針對咱們的需求,這些插件有時候不能達到咱們最理想的效果,本身寫呢,又不會寫,非常頭疼。今天就深刻剖析講解下,教會你們編寫本身的圖片壓縮、方向糾正插件,以及預覽和上傳壓縮後圖片數據。react
文中用到的一些H5的api和EXIF.js等知識點若是不懂的話,請先閱讀文末尾的結語中的知識點資料。android
壓縮圖片而且上傳主要用到filereader、canvas 以及 formdata 這三個h5的api和插件EXIF.js。邏輯並不難。整個過程就是:git
(1)用戶使用input file上傳圖片的時候,用filereader讀取用戶上傳的圖片數據(base64格式)github
(2)把圖片數據傳入img對象,而後將img繪製到canvas上,用EXIF.js對圖片方向進行糾正,再調用canvas.toDataURL對圖片進行壓縮,獲取到壓縮後的base64格式圖片數據,轉成二進制ajax
(3)獲取到壓縮後的圖片二進制數據,預覽。canvas
(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'); 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){ if (navigator.userAgent.match(/iphone/i)) { //console.log('iphone'); //若是方向角不爲1,都須要進行旋轉 if(Orientation != "" && Orientation != 1){ switch(Orientation){ case 6://須要順時針(向左)90度旋轉 rotateImg(img,'left',canvas); break; case 8://須要逆時針(向右)90度旋轉 rotateImg(img,'right',canvas); break; case 3://須要180度旋轉 rotateImg(img,'right',canvas);//轉兩次 rotateImg(img,'right',canvas); break; } }else{ //不作旋轉 rotateImg(img,'no',canvas); } }else if (navigator.userAgent.match(/Android/i)) {// 修復android if(Orientation != "" && Orientation != 1){ switch(Orientation){ case 6://須要順時針(向左)90度旋轉 rotateImg(img,'left',canvas); break; case 8://須要逆時針(向右)90度旋轉 rotateImg(img,'right',canvas); break; case 3://須要180度旋轉 rotateImg(img,'right',canvas);//轉兩次 rotateImg(img,'right',canvas); break; } }else{ //不作旋轉 rotateImg(img,'no',canvas); } }else{ if(Orientation != "" && Orientation != 1){ switch(Orientation){ case 6://須要順時針(向左)90度旋轉 rotateImg(img,'left',canvas); break; case 8://須要逆時針(向右)90度旋轉 rotateImg(img,'right',canvas); break; case 3://須要180度旋轉 rotateImg(img,'right',canvas);//轉兩次 rotateImg(img,'right',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; } canvas.width = width; canvas.height = height; // 鋪底色 ctx.fillStyle = "#fff"; ctx.fillRect(0, 0, width, height); var step = 2; if (step == null) { step = min_step; } if (direction == 'no'){ step = 0; } else if (direction == 'right') { step++; //旋轉到原位置,即超過最大值 step > max_step && (step = min_step); } else { step--; step < min_step && (step = max_step); } //旋轉角度以弧度值爲參數 var degree = step * 90 * Math.PI / 180; switch (step) { case 0: ctx.drawImage(img, 0, 0,width,height); break; case 1: ctx.rotate(degree); ctx.drawImage(img, 0, -height,width,height);