前段時間項目上有一個拍照的需求,對於客戶端固然是個小問題,可是PM要求該功能須要在網頁版的頁面上一樣要實現跟客戶端同樣的體驗!看到這個需求有點蒙,首先還不肯定網頁如何調用系統相機,選本地照片的話弄個<input type="file">應該就ok,其次手機拍一張照片都是幾兆幾兆的,若是不壓縮一下圖片,在這蛋疼的網絡環境下,基本是沒辦法傳到服務器的,網頁上的環境也就那樣,怎麼作圖片壓縮呢?javascript
一、上傳方式html
通常都是採用FormData提交前端
傳統的<form enctype=」multipart/form-data」 method=」post」 action=」」 target=」upload-form」> 配合 <iframe style=」display:none」 name=」upload-form」></iframe>放到今天已經沒法忍受了,好消息最新XHR2中支持把文件放在Formdata對象中異步提交,只考慮移動端,就能夠捨棄iframe之類的兼容方案了。核心代碼這樣:html5
var xhr = new XMLHttpRequest(); var formData = new FormData(); formData.append('file', input.files[0]); xhr.open('POST', form.action); xhr.send(formData);
並且XHR2中還能夠經過process事件來監聽進度,實現相似進度條的功能,代碼這樣:java
xhr.onprogress = updateProgress; xhr.upload.onprogress = updateProgress; function updateProgress(event) { if (event.lengthComputable) { var percentComplete = event.loaded / event.total; ...... } }
用FormData發送的請求頭中你的Content-Type 會變成這樣 multipart/form-data; boundary=----WebKitFormBoundaryyqVkWF3PcCpAzZp9,若是上傳時要附帶參數也能夠直接append到formData裏。jquery
另一種就是讀取圖片數據轉成Base64編碼或者二進制流提交,配合FormData使用提交android
思路就是用JS把圖片讀到canvas中,而後用canvas.toDataURL()接口輸出畫布的base64編碼,再把base64編碼轉成Blob塞到Formdata裏傳到後端。ios
這裏貼一下twitter和webuploader的圖片上傳邏輯git
send: function() { var owner = this.owner, opts = this.options, xhr = this._initAjax(), blob = owner._blob, server = opts.server, formData, binary, fr; if ( opts.sendAsBinary ) { server += (/\?/.test( server ) ? '&' : '?') + $.param( owner._formData ); binary = blob.getSource(); } else { formData = new FormData(); $.each( owner._formData, function( k, v ) { formData.append( k, v ); }); formData.append( opts.fileVal, blob.getSource(), opts.filename || owner._formData.name || '' ); } if ( opts.withCredentials && 'withCredentials' in xhr ) { xhr.open( opts.method, server, true ); xhr.withCredentials = true; } else { xhr.open( opts.method, server ); } this._setRequestHeader( xhr, opts.headers ); if ( binary ) { // 強制設置成 content-type 爲文件流。 xhr.overrideMimeType && xhr.overrideMimeType('application/octet-stream'); // android直接發送blob會致使服務端接收到的是空文件。 // bug詳情。 // https://code.google.com/p/android/issues/detail?id=39882 // 因此先用fileReader讀取出來再經過arraybuffer的方式發送。 if ( Base.os.android ) { fr = new FileReader(); fr.onload = function() { xhr.send( this.result ); fr = fr.onload = null; }; fr.readAsArrayBuffer( binary ); } else { xhr.send( binary ); } } else { xhr.send( formData ); } }
// 壓縮前的代碼 ... convertCanvasToBlob:function(e){var t,i,s,n,r,a,o,c;for(n="image/jpeg",t=e.toDataURL(n),i=window.atob(t.split(",")[1]),r=new window.ArrayBuffer(i.length),a=new window.Uint8Array(r),s=0;s<i.length;s++)a[s]=i.charCodeAt(s);return o=window.WebKitBlobBuilder||window.MozBlobBuilder,o?(c=new o,c.append(r),c.getBlob(n)):new window.Blob([r],{type:n})} ... function convertCanvasToBlob(canvas) { var format = "image/jpeg"; var base64 = canvas.toDataURL(format); var code = window.atob(base64.split(",")[1]); var aBuffer = new window.ArrayBuffer(code.length); var uBuffer = new window.Uint8Array(aBuffer); for(var i = 0; i < code.length; i++){ uBuffer[i] = code.charCodeAt(i); } var Builder = window.WebKitBlobBuilder || window.MozBlobBuilder; if(Builder){ var builder = new Builder; builder.append(buffer); return builder.getBlob(format); } else { return new window.Blob([ buffer ], {type: format}); } } 這是它觸屏版上傳前的圖片壓縮邏輯之一,就是在前端把base64轉成二級制數據,這個數據體積相比base64小不少,還能夠塞到formdata中提交,不過不支持android 2及如下,ios 5.1及如下版本的瀏覽器。 我猜你的業務可能也是想實現相似這樣的圖片上傳功能,分析twitter的源碼可能會對你有一些幫助
二、讀取圖片github
//綁定input change事件 $("#photo").unbind("change").on("change",function(){ var file = this.files[0]; if(file){ //驗證圖片文件類型 if(file.type && !/image/i.test(file.type)){ return false; } var reader = new FileReader(); reader.onload = function(e){ //readAsDataURL後執行onload,進入圖片壓縮邏輯 //e.target.result獲得的就是圖片文件的base64 string render(e.target.result); }; //以dataurl的形式讀取圖片文件 reader.readAsDataURL(file); } });
三、前端圖片壓縮
圖片上傳的主體工做算是完成了,不過如今手機隨便拍張照片就是一兩兆,wifi環境下不說,移動網絡經過這方案上傳照片就有點坑了。手機客戶端中通常會先壓縮圖片再上傳,Web中如何實現壓縮後上傳呢?
能夠把圖片讀到canvas中,而後用canvas.toDataURL()接口輸出畫布的base64編碼,再把base64編碼轉成Blob塞到Formdata裏傳到後端。這樣便可以壓縮圖片減小流量,又能夠在前端就修正圖片旋轉的問題。固然這裏面處理兼容的的坑不少,咱們只說思路。
//定義照片的最大高度 var MAX_HEIGHT = 480; var render = function(src){ var image = new Image(); image.onload = function(){ var cvs = document.getElementById("cvs"); var w = image.width; var h = image.height; //計算壓縮後的圖片長和寬 if(h>MAX_HEIGHT){ w *= MAX_HEIGHT/h; h = MAX_HEIGHT; } var ctx = cvs.getContext("2d"); cvs.width = w; cvs.height = h; //將圖片繪製到Canvas上,從原點0,0繪製到w,h ctx.drawImage(image,0,0,w,h); //進入圖片上傳邏輯 sendImg(); }; image.src = src; };
四、上傳圖片
var sendImg = function(){ var cvs = document.getElementById("cvs"); //調用Canvas的toDataURL接口,獲得的是照片文件的base64編碼string var data = cvs.toDataURL("image/jpeg"); //base64 string太短顯然就不是正常的圖片數據了,過濾の。 if(data.length<48){ console.log("image data error."); return; } //圖片的base64 string格式是data:/image/jpeg;base64,xxxxxxxxxxx //是以data:/image/jpeg;base64,開頭的,咱們在服務端寫入圖片數據的時候不須要這個頭! //因此在這裏只拿頭後面的string //固然這一步能夠在服務端作,但讓閒着蛋疼的客戶端幫着作一點吧~~~(稍微減輕一點服務器壓力) data = data.split(",")[1]; $.post("./api/uploadimg",{ fileName:"xxx.jpeg", fileData:data },function(data){ if(data.status==200){ // some code here. console.log("commit image success."); }else{ console.log("commit image failed."); } },"json"); };
看完上面的代碼,是否是以爲也沒那麼難?真的是這樣嗎?code旅途艱辛,顯然沒那麼容易就讓你好過。
測試後發現,在pc上以及大部分android和iphone4s+上是正常的,可是極小部分android和iphone4s如下的機型上獲得的照片竟然是不完整的!好比只有上半部分,下半部分是黑的,或者照片是旋轉的!開始覺得是服務端圖片存儲的時候出了問題,不事後面排除了服務端的問題,看來上面代碼是有兼容性問題的。
具體排除問題的過程很複雜糾結,就不細說了。貼幾個帖子:
1.HTML5 Canvas drawImage ratio bug iOS
2.iOS HTML5 canvas drawImage vertical scaling bug, even for small images?
3.Drawing on canvas after megapix rendering is reversed
主要是低版本的ios safari上面對於大尺寸的照片(超過設備的物理像素)處理的bug,致使的現象就是上半部分是照片下半部分是黑的,咱們須要一個工具將一張大圖切成若干個小於屏幕尺寸的小圖,分別對小圖進行處理而後再合併成一張圖片。原理很簡單,但實現起來就沒那麼簡單了,仍是已經有相關的開源工具來完成這個工做。
剩下一個圖片旋轉的問題,其實每張圖片拍攝後EXIF
裏面都帶有旋轉Orientation
字段來標註圖片的旋轉信息的,也就是說其實圖片自己就是倒着的,可是圖片展現的時候經過讀取Orientation
來修正圖片展現,使圖片能按照拍攝的角度展現,因此咱們在寫入圖片數據的時候須要按照圖片自己的Orientation
來寫入數據,這樣咱們就須要拿到圖片自己的EXIF
信息。
JavaScript library for reading EXIF image metadata
四、實際測試一下iOS沒問題,Android 4 有些機型不行,貌似修改過file的Blob數據發到服務端的數據字節就會爲0 這是安卓的bug https://code.google.com/p/android/issues/detail?id=39882 。 網上有人給出的解決方案是用FileReader把文件讀出來,而後把整個二進制文件當請求發到服務端,這種方式要附帶參數的話只能放url裏了。
var reader = new FileReader(); reader.onload = function() { $.ajax({ type: 'POST', url: server, data: this.result, contentType: false, processData: false, beforeSend: function (xhr) { xhr.overrideMimeType('application/octet-stream'); }, }).done(function (res) { ...... }).fail(function () { ...... }).always(function () { ...... }); }; reader.readAsArrayBuffer(file);
ok,問題終於所有排除完畢啦。那麼通過優化後的完整代碼就是:
//綁定input change事件 $("#photo").unbind("change").on("change",function(){ var file = this.files[0]; if(file){ //驗證圖片文件類型 if(file.type && !/image/i.test(file.type)){ return false; } var reader = new FileReader(); reader.onload = function(e){ //readAsDataURL後執行onload,進入圖片壓縮邏輯 //e.target.result獲得的就是圖片文件的base64 string render(file,e.target.result); }; //以dataurl的形式讀取圖片文件 reader.readAsDataURL(file); } }); //定義照片的最大高度 var MAX_HEIGHT = 480; var render = function(file,src){ EXIF.getData(file,function(){ //獲取照片自己的Orientation var orientation = EXIF.getTag(this, "Orientation"); var image = new Image(); image.onload = function(){ var cvs = document.getElementById("cvs"); var w = image.width; var h = image.height; //計算壓縮後的圖片長和寬 if(h>MAX_HEIGHT){ w *= MAX_HEIGHT/h; h = MAX_HEIGHT; } //使用MegaPixImage封裝照片數據 var mpImg = new MegaPixImage(file); //按照Orientation來寫入圖片數據,回調函數是上傳圖片到服務器 mpImg.render(cvs, {maxWidth:w,maxHeight:h,orientation:orientation}, sendImg); }; image.src = src; }); }; //上傳圖片到服務器 var sendImg = function(){ var cvs = document.getElementById("cvs"); //調用Canvas的toDataURL接口,獲得的是照片文件的base64編碼string var data = cvs.toDataURL("image/jpeg"); //base64 string太短顯然就不是正常的圖片數據了,過濾の。 if(data.length<48){ console.log("data error."); return; } //圖片的base64 string格式是data:/image/jpeg;base64,xxxxxxxxxxx //是以data:/image/jpeg;base64,開頭的,咱們在服務端寫入圖片數據的時候不須要這個頭! //因此在這裏只拿頭後面的string //固然這一步能夠在服務端作,但讓閒着蛋疼的客戶端幫着作一點吧~~~(稍微減輕一點服務器壓力) data = data.split(",")[1]; $.post("./api/uploadimg",{ fileName:"xxx.jpeg", fileData:data },function(data){ if(data.status==200){ // some code here. console.log("commit image success."); }else{ console.log("commit image failed."); } },"json"); };
實測一下,稍低端的的安卓上有點卡,畢竟處理一張圖片的運算量可不小,目測目前用前端壓縮上傳方案的很少,至少微博觸屏版 (http://m.weibo.cn/) 就是把原始圖片直接上傳的,這種方式是否適合直接使用或者還有哪些能夠優化的地方有待驗證。QQ空間觸屏版圖片上傳是直接把圖片base64編碼發給服務端處理。
參考: