需求:前端上傳圖片的時候通常需要提供指定大小以內的圖片。比如不大於500KB。
思路:利用canvas轉blob的時候通過quality控制圖片質量,達到壓縮的目的。此方法有個缺點。只能對圖片格式爲jpeg或webp的圖片有效。因此壓縮的時候canvas.toBlob(callback, mimeType, quality)中的mimeType要設爲'image/jpeg'。壓縮完成可以自行轉成想要的格式。這裏最主要的是找到小於maxSize並且最接近maxSize的圖片質量參數quality。
效果圖:用進度條模擬壓縮的進度。支持同時上傳多張圖片同時壓縮
代碼如下:
import React from 'react';
import PropTypes from 'prop-types';
import styles from './upload.less';
import compress from './compress';
class Upload extends React.Component {
constructor(props) {
super(props);
this.fileInput = React.createRef();
this.state = {
fileObjs: [], // item { originFile, compressBase64, compressFile }
};
}
getFileUrl(file) {
let url;
const agent = navigator.userAgent;
if (agent.indexOf('MSIE') >= 1) {
url = file.value;
} else if (agent.indexOf('Firefox') > 0 || agent.indexOf('Chrome') > 0) {
url = window.URL.createObjectURL(file);
}
return url;
}
compressCallBack(file, fileObj, result) {
const { fileObjs } = this.state;
file.compressing = false; // 壓縮完成
fileObj.compressBase64 = result.compressBase64;
fileObj.compressFile = result.compressFile;
this.setState({ fileObjs: [...fileObjs] });
if (fileObjs.length && fileObjs.every(fileObjItem => fileObjItem.compressBase64)) {
console.log('全部壓縮完成', fileObjs);
}
}
onInputChange(e) {
const { fileObjs } = this.state;
Object.keys(e.target.files).forEach((key) => {
const file = e.target.files[key];
// 驗證圖片格式
const type = file.name.split('.')[1];
if (type !== 'png' && type !== 'jpg' && type !== 'jpeg') {
console.warn('請上傳png,jpg,jpeg格式的圖片!');
e.target.value = '';
return;
}
file.url = this.getFileUrl(file);
file.compressing = true; // 壓縮狀態,開始壓縮
const fileObj = { originFile: file, compressBase64: null, compressFile: null };
fileObjs.push(fileObj);
// 壓縮圖片的方法, maxSize單位爲kb
compress(file, 200).then((res) => {
this.compressCallBack(file, fileObj, res);
}, (err) => {
// 壓縮失敗,則返回原圖片的信息
this.compressCallBack(file, fileObj, err);
});
});
this.setState({ fileObjs: [...fileObjs] });
e.target.value = '';
}
render() {
const { fileObjs } = this.state;
return (
<div
className={styles.uploadContainer}
>
<div
className={styles.inputContainer}
onClick={() => {
this.fileInput.current.click();
}}
>
<span className={styles.uploadIcon}>+</span>
<input
className={styles.fileInput}
ref={this.fileInput}
type="file"
name="file"
multiple="multiple"
accept="image/png,image/jpg,image/jpeg"
onChange={e => this.onInputChange(e)}
/>
</div>
{
fileObjs.map(fileObj => (
<div className={styles.imgContainer}>
<img
src={fileObj.compressBase64 ? fileObj.compressBase64 : fileObj.originFile.url}
className={fileObj.originFile.compressing && styles.filter}
/>
{
fileObj.originFile.compressing ?
<div className={styles.progressContainer}>
<div className={styles.progress}>
<div className={styles.progressHighlight} />
</div>
</div> : ''
}
</div>
))
}
</div>);
}
}
export default Upload;
2.圖片壓縮主要代碼compress.js
// 將File(Blob)對象轉變爲一個dataURL字符串, 即base64格式
const fileToDataURL = file => new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = e => resolve(e.target.result);
reader.readAsDataURL(file);
});
// 將dataURL字符串轉變爲image對象,即base64轉img對象
const dataURLToImage = dataURL => new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img);
img.src = dataURL;
});
// 將一個canvas對象轉變爲一個File(Blob)對象
const canvastoFile = (canvas, type, quality) => new Promise(resolve =>
canvas.toBlob(blob => resolve(blob), type, quality));
const compress = (originfile, maxSize) => new Promise(async (resolve, reject) => {
const originSize = originfile.size / 1024; // 單位爲kb
// 將原圖片轉換成base64
const base64 = await fileToDataURL(originfile);
// 縮放圖片需要的canvas
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// 小於maxSize,則不需要壓縮,直接返回
if (originSize < maxSize) {
resolve({ compressBase64: base64, compressFile: originfile });
console.log(`圖片小於指定大小:${maxSize}KB,不用壓縮`);
return;
}
const img = await dataURLToImage(base64);
const scale = 1;
const originWidth = img.width;
const originHeight = img.height;
const targetWidth = originWidth * scale;
const targetHeight = originHeight * scale;
canvas.width = targetWidth;
canvas.height = targetHeight;
context.clearRect(0, 0, targetWidth, targetHeight);
context.drawImage(img, 0, 0, targetWidth, targetHeight);
// 將Canvas對象轉變爲dataURL字符串,即壓縮後圖片的base64格式
// const compressedBase64 = canvas.toDataURL('image/jpeg', 0.1);
// 經過我的對比,通過scale控制圖片的拉伸來壓縮圖片,能夠壓縮jpg,png等格式的圖片
// 通過canvastoFile方法傳遞quality來壓縮圖片,只能壓縮jpeg類型的圖片,png等格式不支持
// scale的壓縮效果沒有canvastoFile好
// 在壓縮到指定大小時,通過scale壓縮的圖片比通過quality壓縮的圖片模糊的多
// 壓縮的思路:由於quality參數的精度爲0.01,因此我們只需要從0.9開始,一直遞減:0.8, 0.7, 0.6, 0.5,
// 0.4, 0.3, 0.2, 0.1, 0;找到第一個小於maxSize的quality。假如第一個小於maxSize的quality爲0.4,則
// 繼續遞增0.41, 0.42, 0.43, 0.44,當遞增到0.45是,壓縮的圖片開始大於maxSize,則此時我們可以斷定
// quality = 0.44時,壓縮出來的圖片大小最接近maxSize。這種算法比較笨,但是也能涵蓋所有的情況。
// 這裏爲了規避浮點數計算的弊端,將quality轉爲整數再計算;
let preQuality = 100;
let quality = 90;
let count = 0; // 嘗試壓縮次數
let compressFinish = false; // 壓縮完成
let invalidDesc = '';
let compressBlob = null;
// 找到最接近maxSize的quality
while (!compressFinish) {
compressBlob = await canvastoFile(canvas, 'image/jpeg', quality / 100);
const compressSize = compressBlob.size / 1024;
count++;
console.log(quality / 100, compressSize);
if (maxSize >= compressSize) {
preQuality = quality;
quality += 1;
} else {
if (preQuality !== 100) {
compressFinish = true;
}
if (!quality) {
// 當quality等於0,並且壓縮後的圖片還是比指定的大小大,說明無法壓縮
compressFinish = true;
invalidDesc = '壓縮失敗,無法壓縮到指定大小';
}
quality -= 10;
}
}
if (invalidDesc) {
// 壓縮失敗,則返回原始圖片的信息
console.log(`壓縮失敗,無法壓縮到指定大小:${maxSize}KB`)
reject({ msg: invalidDesc, compressBase64: base64, compressFile: originfile });
return;
}
compressBlob = await canvastoFile(canvas, 'image/jpeg', preQuality / 100);
const compressedBase64 = await fileToDataURL(compressBlob);
const compressedFile = new File([compressBlob], originfile.name, { type: 'image/jpeg' });
console.log(`壓縮完成,總共嘗試了${count}次`);
resolve({ compressFile: compressedFile, compressBase64: compressedBase64 });
});
export default compress;
3.less
.uploadContainer{
display: flex;
flex-wrap: wrap;
.inputContainer{
display: flex;
align-items: center;
justify-content: center;
width: 100px;
height: 100px;
//background:rgba(245,250,255,1);
border-radius:8px;
border:1px solid rgba(217,217,217,1);
margin-right: 10px;
.fileInput{
display: none;
}
.uploadIcon{
font-size: 30px;
color: lightgrey;
}
}
.imgContainer{
position: relative;
width: 100px;
height: 100px;
margin-right: 10px;
&:last-child{
margin-right: 10px;
}
img{
width: 100%;
height: 100%;
}
.filter{
filter: blur(1px);
}
.progressContainer{
position: absolute;
width: 80%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: 10px;
.progress{
width: 100%;
height: 4px;
border-radius: 3px;
border: 1px solid rgba(0,0,0,0.1);
}
.progressHighlight{
height: 100%;
width: 100%;
animation: progress 3s cubic-bezier(0.25,0.1,0.25,1) infinite;
background: orange;
border-radius: 3px;
}
}
}
}
@keyframes progress
{
0% {width: 0}
to {width: 100%}
}