前端图片压缩
- 需求:最近做项目,要求900k-5M的图片,需要压缩到900K以下
- 实现:利用
canvas.toDataURL(type, encoderOptions)
或toBlob(callback, type, encoderOptions)
实现
前置知识点
canvas.toDataURL(type, encoderOptions)
,直接照搬MDN的文档描述。总的来说,想要用这个方法压缩图片,调用toDataURL和toBlob的时候,type只能jpeg或者webp的格式来处理,不然压缩是无效的。最后输出的时候是可以换成任意格式的图片的,比如我就改成了png格式的图片输出。
- 如果画布的高度或宽度是0,那么会返回字符串“data:,”。
- 如果传入的类型非“image/png”,但是返回的值以“data:image/png”开头,那么该传入的类型是不支持的。
- Chrome支持“image/webp”类型。
- 在指定图片格式为 image/jpeg 或 image/webp的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。
canvas.toBlob(callback, type, encoderOptions)
,这个方法用起来比上面简单一点,因为上面的方法需要处理base64的URL转换成bufferArray,这个直接使用blob对象去初始化File就可以。
callback
,回调函数,可获得一个单独的Blob对象参数。
type
,DOMString类型,指定图片格式,默认格式为image/png。
encoderOptions
,Number类型,值在0与1之间,当请求图片格式为image/jpeg或者image/webp时用来指定图片展示质量。如果这个参数的值不在指定类型与范围之内,则使用默认值,其余参数将被忽略。
atob(base64Str)
,将base64
的字符串解码,与btoa(str)
功能相反,因为toDataURL
返回的是一个base64
编码的地址,需要将其解析并转化成文件。
String.prototype.charCodeAt()
,返回 0 到 65535 之间的整数,表示给定索引处的 UTF-16 代码单元。
代码实现
// 使用toBlob
// file,需要压缩的file对象
// quality,压缩的初始质量
// targetSize,需要压缩的临界值,当压缩一次后比这个临界值要大,就自动递归压缩
function compress(file, quality = 0.9, targetSize = 900 * 1024) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
const src = e.target.result;
const image = new Image();
image.src = src;
image.onload = () => {
const canvas = document.createElement('canvas');
const width = image.width;
const height = image.height;
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
context.drawImage(image, 0, 0, width, height);
canvas.toBlob((blob) => {
// 将文件名后缀改成png,并修改文件格式为png格式
const miniFile = new File([blob], file.name.replace(/.[a-zA-Z]+$/g, '.png'), { type: 'image/png' })
if(miniFile.size > targetSize) {
compress(file, quality * 0.9, targetSize).then(mini => resolve(mini));
} else {
resolve(miniFile);
}
}, 'image/jpeg', quality);
}
image.onerror = (err) => {
reject(err);
}
}
reader.onerror = (err) => {
reject(err);
}
})
}
// 使用toDateURL
function compress(file, quality = 0.9, targetSize = 900 * 1024) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
const src = e.target.result;
const image = new Image();
image.src = src;
image.onload = () => {
const canvas = document.createElement('canvas');
const width = image.width;
const height = image.height;
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
context.drawImage(image, 0, 0, width, height);
const canvasURL = canvas.toDataURL('image/jpeg', quality);
const buffer = atob(canvasURL.split(',')[1]) // 获取实际的文件内容
let length = buffer.length
// 将每一块的内容映射成对应的unicode编码
const bufferArray = new Uint8Array(new ArrayBuffer(length)).map((item, index) => buffer.charCodeAt(index))
// 将文件名后缀改成png,并修改文件格式为png格式
const miniFile = new File([bufferArray], file.name.replace(/.[a-zA-Z]+$/g, '.png'), { type: 'image/png' })
if(miniFile.size > targetSize) {
compress(file, quality * 0.9, targetSize).then(mini => resolve(mini));
} else {
resolve(miniFile);
}
}
image.onerror = (err) => {
reject(err);
}
}
reader.onerror = (err) => {
reject(err);
}
})
}
// 最近又发现可以通过尺寸压缩,实现起来更简单
// ratio尺寸压缩比,需要压缩多少倍
function compress(file, ratio) {
return new Promise((resolve, reject) => {
const size = file.size;
const suffix = file.name.match(/.w+$/g)[0];
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function(e) {
const src = e.target.result;
const img = new Image();
img.src = src;
img.onload = function() {
const w = img.width,
h = img.height;
const canvas = document.createElement('canvas');
canvas.width = w / ratio;
canvas.height = h / ratio;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, w / ratio, h / ratio);
// 这里依然可以使用toBlob和toDataURL两种方式
const url = canvas.toDataURL(`image/${suffix}`);
const binary = btoa(url.split(',')[1]);
const bufferArray = new Uint8Array(new ArrayBuffer(binary.length)).map((item, index) => binary.charCodeAt(index));
const miniFile = new File([bufferArray], file.name, { type: `image/${suffix}` });
resolve(miniFile)
}
img.onerror = function() {
reject('Img load fail.')
}
}
reader.onerror = function() {
reject('Reader fail.')
}
})
}
// 调用
compress(file).then(miniFile=> {
console.log(miniFile);
}).catch(err => {
console.log(err)
})
注意点
- 压缩很难精确到固定的尺寸,如果对于临界尺寸有偏差值的要求,需要在递归条件上自己拓展,目前只要小于临界值就可以,如果需要更精确,可以改成一个尺寸的范围判断,如果太小了,就把压缩比例再上调,直到满足条件为止。
- 目前压缩过后的文件类型我自己改成了png,如果有其它要求,或者想动态的传递格式,可以自行拓展该方法,将该参数提炼出来。
- 当递归的次数比较多的时候,可能就会比较耗时,所以建议采用二分法去获取最佳尺寸,但是依然会比较耗时,如果考虑到界面交互的话,可以先展示原图加一个filter的blur,然后等待压缩完成后替换成压缩后的图片。