移動端圖片壓縮上傳實現(xiàn)
移動端上傳的圖片一般都是手機(jī)照片体箕,現(xiàn)在的手機(jī)都是高清像素专钉,一張圖片都在三四兆,直接上傳不僅傳輸速度慢累铅,而且如果用戶使用的是流量跃须,勢必會耗費(fèi)大量流量。
H5的各種API在移動端的主流瀏覽器都得到了很好的支持娃兽,比如案例中用到的FileReader菇民、Blob、FormData投储、canvas等API第练,所以壓縮上傳圖片在前端已經(jīng)是必備的操作。
壓縮上傳基本操作流程:
- 圖片上傳后使用FileReader將文件讀取成base64
- 創(chuàng)建Image玛荞,設(shè)置src屬性為圖片base64
- 創(chuàng)建canvas娇掏,繪制Image
- 調(diào)用canvas的toDataURL方法壓縮,返回壓縮后的base64
- 將base64轉(zhuǎn)成Blob對象
- 創(chuàng)建FormData對象冲泥,append Blob對象驹碍,提交給服務(wù)端
下面是每一步的具體實現(xiàn)以及一些坑(比如:API的兼容性、IOS圖片旋轉(zhuǎn)凡恍、底色等)志秃,并貼上全部代碼。
<input type="file" id="upload" accept='image/*'>
<h3>調(diào)用系統(tǒng)錄制功能</h3>
<input name='video' type='file' id='video-input' accept='video/*' capture='camcorder' />
<h3>調(diào)用系統(tǒng)相機(jī)</h3>
<input name='video' type='file' id='video-input' accept='image/*' capture='camcorder' />
import EXIF from 'exif';
一嚼酝、監(jiān)聽input的change事件浮还,讀取成base64。如果照片是豎著拍的闽巩,在IOS手機(jī)上傳后圖片會被旋轉(zhuǎn)钧舌。這里需要用到一個庫EXIF),可以獲取相片的屬性涎跨,比如曝光度洼冻、拍照方向、GPS等隅很。圖片加載完成后撞牢,在壓縮前需要解決IOS圖片是否被旋轉(zhuǎn)的問題和圖片壓縮格式的問題。
-
圖片旋轉(zhuǎn)
- 問題: IOS豎著拍的照片會旋轉(zhuǎn)叔营。
- 解決: 首先創(chuàng)建臨時canvas屋彪,繪制圖片,旋轉(zhuǎn)成正確方向
canvas的toDataURL() 參數(shù)type的默認(rèn)值是 “image/png”绒尊,如果傳入的類型非“image/png”畜挥,但是返回的值以“data:image/png”開頭,那么該傳入的類型是不支持的婴谱。把類型統(tǒng)一設(shè)成jpeg蟹但,也就是統(tǒng)一用canvas.toDataURL('image/jpeg', 0.3) 躯泰,壓縮默認(rèn)值 0.92,這里我設(shè)的0.3矮湘。
let inp = document.getElementById('upload');
inp.onchange = function (event) {
let file = event.target.files[0];
let reader = new FileReader();
let Orientation;
// 讀取文件轉(zhuǎn)base64
reader.readAsDataURL(file);
// 讀取完成
reader.onload = function () {
let result = this.result;
/**
* result.length 的單位是字節(jié)
* 如果圖片小于100K直接上傳斟冕,反之壓縮圖片
*/
if (result.length <= (100 * 1024)) {
// 直接上傳 調(diào)用API
}
else {
// 創(chuàng)建image
let image = new Image();
image.src = result;
// 圖片加載完成
image.onload = function () {
//獲取拍照的信息,解決IOS拍出來的照片旋轉(zhuǎn)問題
EXIF.getData(image, function () {
Orientation = EXIF.getTag(this, 'Orientation');
});
// 首先旋轉(zhuǎn)成正確位置 再根據(jù)大小壓縮 然后根據(jù)像素判斷是否需要通過瓦片繪制
let canvas;
// 修復(fù)ios拍照上傳圖片的時被旋轉(zhuǎn)的問題
if (Orientation !== '' && Orientation !== 1) {
// 創(chuàng)建臨時canvas 用來調(diào)整正確方位
canvas = document.createElement('canvas');
switch (Orientation) {
case 6://需要順時針(向左)90度旋轉(zhuǎn)
console.log(image.width, image.height);
rotateImg(image, 'left', canvas);
break;
case 8://需要逆時針(向右)90度旋轉(zhuǎn)
rotateImg(image, 'right', canvas);
break;
case 3://需要180度旋轉(zhuǎn)
rotateImg(image, 'right', canvas);//轉(zhuǎn)兩次
rotateImg(image, 'right', canvas);
break;
}
}
else {
canvas = compress(image);
}
// 對縮小比例后的canvas再進(jìn)行壓縮
let compressData = canvas.toDataURL("image/jpeg", 0.3); // 默認(rèn)MIME image/png
let blob = convertBase64UrlToBlob(compressData);
// 提交數(shù)據(jù)
submitFormData(blob);
}
}
}
}
二缅阳、 解決了圖片旋轉(zhuǎn)和圖片格式問題在壓縮前需要解決canvas繪制圖片的兩個限制和圖片格式轉(zhuǎn)換的問題磕蛇。
-
兩個限制
- 問題
第一是圖片的大小:如果圖片的大小超過兩百萬像素十办,圖片也是無法繪制到canvas上的秀撇,調(diào)用drawImage的時候不會報錯,但是你用toDataURL獲取圖片數(shù)據(jù)的時候獲取到的是空的圖片數(shù)據(jù)向族。
第二是canvas的大小有限制呵燕,如果canvas的大小大于大概五百萬像素(寬 * 高)時,不僅圖片畫不出來件相,其他什么東西也都是畫不出來的再扭。
- 解決方法
第一種限制,處理辦法就是瓦片繪制夜矗。瓦片繪制泛范,也就是將圖片分割成多塊繪制到canvas上,代碼里的實現(xiàn)是把圖片分割成100萬像素一塊的大小紊撕,再繪制到canvas上罢荡。
第二種限制,對圖片的寬高進(jìn)行適當(dāng)壓縮对扶。具體實現(xiàn)以上限四百萬像素為基準(zhǔn)区赵,如果圖片大于四百萬像素就壓縮到小于四百萬像素。
如果是png轉(zhuǎn)jpg浪南,繪制到canvas上的時候笼才,canvas存在透明區(qū)域的話,當(dāng)轉(zhuǎn)成jpg的時候透明區(qū)域會變成黑色络凿,因為canvas的透明像素默認(rèn)為rgba(0,0,0,0)患整,所以轉(zhuǎn)成jpg就變成rgba(0, 0, 0 ,1)了,也就是透明背景會變成了黑色喷众。解決辦法就是繪制之前在canvas上鋪一層白色的底色。
function compress(image) {
let {width, height} = image;
// 創(chuàng)建canvas 獲取上下文
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
/**
* 判斷像素大小
* 像素 = 寬 * 高
*/
// 如果像素大于400萬 則需計算壓縮比 壓縮至400萬以下
let ratio = (width * height) / 4000000;
if (ratio > 1) {
// 倍數(shù)開方 (相當(dāng)于面積為多少倍紧憾,則寬高對應(yīng)的倍數(shù)需對面積倍數(shù)開方)
ratio = Math.sqrt(ratio);
// 寬高對應(yīng)的值
width /= ratio;
height /= ratio;
}
else {
ratio = 1;
}
// 畫布寬高
canvas.width = width;
canvas.height = height;
// 鋪底色
ctx.fillStyle = '#fff';
// 繪制矩形
ctx.fillRect(0, 0, width, height);
// 如果縮放比例后畫布像素仍大于100萬像素 則使用瓦片繪制到千, 反之直接繪制
let count = width * height / 1000000;
if (count > 1) {
// 創(chuàng)建瓦片 獲取2d上下文
let tcanvas = document.createElement('canvas');
let tctx = tcanvas.getContext('2d');
/**
* 瓦片數(shù)量 = count的平方 + 1
* +1不是必須得,是為了瓦片更小赴穗,數(shù)量更多一些
*/
count = ~~(Math.sqrt(count) + 1); // 比如count為2.3 則轉(zhuǎn)成3
let tWidth = ~~(width / count);
let tHeight = ~~(height / count);
// 瓦片的寬高
tcanvas.width = tWidth;
tcanvas.height = tHeight;
for (let i = 0; i < count; i++) {
for (let j = 0; j < count; j++) {
tctx.drawImage(image, i * tWidth * ratio, j * tHeight * ratio, tWidth * ratio, tHeight * ratio, 0, 0, tWidth, tHeight);
console.log(tcanvas.width, tcanvas.height, tcanvas.width * tcanvas.height);
ctx.drawImage(tcanvas, i * tWidth, j * tHeight, tWidth, tHeight);
}
}
}
else {
// 直接繪制
ctx.drawImage(image, 0, 0, width, height);
}
return canvas;
}
- 完成圖片壓縮后憔四,先將base64提取出來膀息,再實例化一個ArrayBuffer,然后將字符串以8位整型的格式傳入ArrayBuffer了赵,再通過Blob對象(可能需要兼容Blob)潜支,將8位整型的ArrayBuffer轉(zhuǎn)成二進(jìn)制對象blob,然后把blob對象append到formdata里柿汛,再提交給后臺冗酿。
function convertBase64UrlToBlob(urlData) {
let bytes = window.atob(urlData.split(',')[1]);
// 處理異常,將ascii碼小于0的轉(zhuǎn)換為大于0
let ab = new ArrayBuffer(bytes.length);
let ia = new Uint8Array(ab);
for (let i = 0; i < bytes.length; i++) {
ia[i] = bytes.charCodeAt(i);
}
// 二進(jìn)制對象
return getBlob([ab], "image/jpeg");
}
- 兼容Blob對象
/**
* Blob對象的兼容性寫法
* @param buffer 數(shù)據(jù)流
* @param format 表示將會被放入到blob中的數(shù)組內(nèi)容的MIME類型。類型默認(rèn) ''
*/
function getBlob(buffer, format = 'image/jpeg') {
try {
return new Blob(buffer, {
type: format
});
}
catch (e) {
let blob = new (window.BlobBuilder || window.WebKitBlobBuilder || window.MSBlobBuilder)();
buffer.forEach(function (buf) {
blob.append(buf);
});
return blob.getBlob(format);
}
}
- 低版本的Android機(jī)不支持FormData络断,需要做兼容處理裁替。首先判斷是否需要兼容
function needsFormDataShim() {
return ~navigator.userAgent.indexOf('Android')
&& ~navigator.vendor.indexOf('Google')
&& !~navigator.userAgent.indexOf('Chrome')
&& navigator.userAgent.match(/AppleWebKit\/(\d+)/).pop() <= 534;
}
- 給不支持FormData上傳Blob的android機(jī)打補(bǔ)丁,定義boundary分隔符貌笨,設(shè)置請求體弱判。重寫XMLHttpRequest原型的send方法。
function FormDataShim() {
let o = this,
// 請求體
parts = [],
// 分隔符
boundary = Array(5).join('-') + (+new Date() * (1e16 * Math.random())).toString(36),
oldSend = XMLHttpRequest.prototype.send;
this.append = function (name, value, filename) {
parts.push(`--${boundary}\r\nContent-Disposition: form-data; name="${name}"`);
if (value instanceof Blob) {
parts.push(`; filename="${filename || 'blob'}"\r\nContent-Type: ${value.type}\r\n\r\n`);
parts.push(value);
}
else {
parts.push('\r\n\r\n' + value);
}
parts.push('\r\n');
};
// override XHR send()
XMLHttpRequest.prototype.send = function (val) {
let fr,
data,
oXHR = this;
if (val === o) {
// 不能漏最后的\r\n ,否則服務(wù)器有可能解析不到參數(shù).
parts.push(`--${boundary}--\r\n`);
// 創(chuàng)建Blob對象
data = getBlob(parts);
// Set up and read the blob into an array to be sent
fr = new FileReader();
fr.onload = function () {
oldSend.call(oXHR, fr.result);
};
fr.onerror = function (err) {
throw err;
};
fr.readAsArrayBuffer(data);
// 設(shè)置請求頭Content-Type的類型和分隔符 服務(wù)端是根據(jù)Content-Type來解析請求體中
this.setRequestHeader(
'Content-Type',
`multipart/form-data; boundary=${boundary}`
);
XMLHttpRequest.prototype.send = oldSend;
}
else {
oldSend.call(this, val);
}
};
}
- 提交數(shù)據(jù)锥惋。判斷是否支持FormData
function submitFormData(blob) {
let isNeedShim = needsFormDataShim();
let formdata = isNeedShim ? new FormDataShim() : new FormData();
formdata.append('imagefile', blob);
if (isNeedShim) {
let ajax = new XMLHttpRequest();
ajax.open('POST', '/');
ajax.onreadystatechange = function() {
if (ajax.status === 200 && ajax.readyState === 4) {
}
}
ajax.send(formdata);
}
else {
// 調(diào)用API
axios.post('/upload', formdata)
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error);
});
// axios 會根據(jù)提交的文件類型昌腰,設(shè)置相應(yīng)的Content-Type類型
}
}
- 旋轉(zhuǎn)圖片
/**
* @param 旋轉(zhuǎn)的圖片
* @param 方向
* @param 繪制的canvas
*/
function rotateImg(img, direction, canvas) {
//最小與最大旋轉(zhuǎn)方向,圖片旋轉(zhuǎn)4次后回到原方向
const min_step = 0;
const max_step = 3;
if (img == null) return;
// 縮小比例后的canvas
let lessCnavas = compress(img);
let {width, height} = lessCnavas;
let step = 2;
if (step == null) {
step = min_step;
}
if (direction == 'right') {
step++;
//旋轉(zhuǎn)到原位置膀跌,即超過最大值
step > max_step && (step = min_step);
} else {
step--;
step < min_step && (step = max_step);
}
//旋轉(zhuǎn)角度以弧度值為參數(shù)
let degree = (step * 90 * Math.PI) / 180;
let ctx = canvas.getContext('2d');
switch (step) {
case 0:
canvas.width = width;
canvas.height = height;
ctx.drawImage(lessCnavas, 0, 0);
break;
case 1:
canvas.width = height;
canvas.height = width;
ctx.rotate(degree);
ctx.drawImage(lessCnavas, 0, -height);
break;
case 2:
canvas.width = width;
canvas.height = height;
ctx.rotate(degree);
ctx.drawImage(lessCnavas, -width, -height);
break;
case 3:
canvas.width = height;
canvas.height = width;
ctx.rotate(degree);
ctx.drawImage(lessCnavas, -width, 0);
break;
}
}
以上就是壓縮上傳的全部實現(xiàn)遭商。