需求:前端上传图片的时候通常需要提供指定大小以内的图片。比如不大于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.gridItem}>
<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/*"
onChange={e => this.onInputChange(e)}
/>
</div>
</div>
{
fileObjs.map(fileObj => (
<div className={styles.gridItem}>
<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>);
}
}
Upload.propTypes = {
dispatch: PropTypes.func.isRequired,
};
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
console.log('图片指定最大尺寸为', maxSize, '原始尺寸为:', originSize);
// 将原图片转换成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转为整数再计算;
// const preQuality = 100;
const maxQualitySize = { quality: 100, size: Number.MAX_SAFE_INTEGER };
const minQualitySize = { quality: 0, size: 0 };
let quality = 100;
let count = 0; // 压缩次数
let compressFinish = false; // 压缩完成
let invalidDesc = '';
let compressBlob = null;
// 二分法最多尝试8次即可覆盖全部可能
while (!compressFinish && count < 12) {
compressBlob = await canvastoFile(canvas, 'image/jpeg', quality / 100);
const compressSize = compressBlob.size / 1024;
count++;
if (compressSize === maxSize) {
console.log(`压缩完成,总共压缩了${count}次`);
compressFinish = true;
return;
}
if (compressSize > maxSize) {
maxQualitySize.quality = quality;
maxQualitySize.size = compressSize;
}
if (compressSize < maxSize) {
minQualitySize.quality = quality;
minQualitySize.size = compressSize;
}
console.log(`第${count}次压缩,压缩后大小${compressSize},quality参数:${quality}`);
quality = Math.ceil((maxQualitySize.quality + minQualitySize.quality) / 2);
if (maxQualitySize.quality - minQualitySize.quality < 2) {
if (!minQualitySize.size && quality) {
quality = minQualitySize.quality;
} else if (!minQualitySize.size && !quality) {
compressFinish = true;
invalidDesc = '压缩失败,无法压缩到指定大小';
console.log(`压缩完成,总共压缩了${count}次`);
} else if (minQualitySize.size > maxSize) {
compressFinish = true;
invalidDesc = '压缩失败,无法压缩到指定大小';
console.log(`压缩完成,总共压缩了${count}次`);
} else {
console.log(`压缩完成,总共压缩了${count}次`);
compressFinish = true;
quality = minQualitySize.quality;
}
}
}
if (invalidDesc) {
// 压缩失败,则返回原始图片的信息
console.log(`压缩失败,无法压缩到指定大小:${maxSize}KB`);
reject({ msg: invalidDesc, compressBase64: base64, compressFile: originfile });
return;
}
compressBlob = await canvastoFile(canvas, 'image/jpeg', quality / 100);
const compressSize = compressBlob.size / 1024;
console.log(`最后一次压缩(即第${count + 1}次),quality为:${quality},大小:${compressSize}`);
const compressedBase64 = await fileToDataURL(compressBlob);
const compressedFile = new File([compressBlob], originfile.name, { type: 'image/jpeg' });
resolve({ compressFile: compressedFile, compressBase64: compressedBase64 });
});
export default compress;
3.less
.uploadContainer{
display: grid;
grid-template-columns: repeat(auto-fill,minmax(75px, 1fr));
grid-row-gap: .10rem;
grid-column-gap: .08rem;
.gridItem{
position: relative;
width: 100%;
height: 0;
padding-top: 100%;
.inputContainer{
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius:8px;
border:1px solid rgba(217,217,217,1);
.fileInput{
display: none;
}
.uploadIcon{
font-size: 30px;
color: lightgrey;
}
}
img{
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border-radius:8px;
}
.delete{
position: absolute;
top: -9px;
right: -9px;
width: 18px;
height: 18px;
background: red;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.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%}
}