共计 4418 个字符,预计需要花费 12 分钟才能阅读完成。
利用 canvas 压缩图片
前言
在一个移动端的项目中,图片上传是一个比较常用的功能。
但是,目前手机的随便拍的照片一张都要好几 M , 直接上传的话特别耗费流量,而且所需时间也比较长。
所以需要前端在上传之前先对图片进行压缩。
原理
要使用 js 实现图片压缩效果,原理其实很简单,主要是:
利用 canvas 的 drawImage 将目标图片画到画布上
利用画布调整绘制尺寸,以及导出的 quality,确定压缩的程度
利用 canvas 的 toDataURL 或者 toBlob 可以将画布上的内容导出成 base64 格式的数据。
注意点
IOS 下会出现图片翻转的问题
这个需要 import EXIF from ‘exif-js’; 来获取到手机的方向,然后对 canvas 的宽高进行处理
压缩到特定大小
let imgDataLength = dataUrl.length; 获取到数据后,判断压缩后的图片大小是否满足需求,否则就降低尺寸以及质量,再次压缩
quality 对 png 等无效,所以导出格式统一为 jpeg,透明背景填充为白色
// 填充白色背景
ctx.fillStyle = fillBgColor;
ctx.fillRect(0, 0, size.w, size.h);
具体源码
/**
* 文件读取并通过 canvas 压缩转成 base64
* @param files
* @param callback
*/
//EXIF js 可以读取图片的元信息 https://github.com/exif-js/exif-js
import EXIF from ‘exif-js’;
// 压缩图片时 质量减少的值
const COMPRESS_QUALITY_STEP = 0.03;
const COMPRESS_QUALITY_STEP_BIG = 0.06;
// 压缩图片时,图片尺寸缩放的比例,eg:0.9, 等比例缩放为 0.9
const COMPRESS_SIZE_RATE = 0.9;
let defaultOptions = {
removeBase64Header: true,
// 压缩后允许的最大值,默认:300kb
maxSize: 200 * 1024,
fillBgColor: ‘#ffffff’
};
/**
* 将待上传文件列表压缩并转换 base64
*!!!!注意:图片会默认被转为 jpeg,透明底会加白色背景
* files : 文件列表,必须是数组
* callback : 回调,每个文件压缩成功后都会回调,
* options:配置
* options.removeBase64Header : 是否需要删除 ‘data:image/jpeg;base64,’ 这段前缀,默认 true
* @return {base64Data: ”,fileType: ”},//fileType 强制改为 jpeg
*/
export function imageListConvert(files, callback, options = {}) {
if (!files.length) {
console.warn(‘files is null’);
return;
}
options = {…defaultOptions, …options};
// 获取图片方向--iOS 拍照下有值
EXIF.getData(files[0], function() {
let orientation = EXIF.getTag(this, ‘Orientation’);
for (let i = 0, len = files.length; i < len; i++) {
let file = files[i];
let fileType = getFileType(file.name);
// 强制改为 jpeg
fileType = ‘jpeg’;
let reader = new FileReader();
reader.onload = (function() {
return function(e) {
let image = new Image();
image.onload = function() {
let data = convertImage(
image,
orientation,
fileType,
options.maxSize,
options.fillBgColor
);
if (options.removeBase64Header) {
data = removeBase64Header(data);
}
callback({
base64Data: data,
fileType: fileType
});
};
image.src = e.target.result;
};
})(file);
reader.readAsDataURL(file);
}
});
}
/**
* 将 image 对象 画入画布并导出 base64 数据
*/
export function convertImage(
image,
orientation,
fileType = ‘jpeg’,
maxSize = 200 * 1024,
fillBgColor = ‘#ffffff’
) {
let maxWidth = 1280,
maxHeight = 1280,
cvs = document.createElement(‘canvas’),
w = image.width,
h = image.height,
quality = 0.9;
/**
* 这里用于计算画布的宽高
*/
if (w > 0 && h > 0) {
if (w / h >= maxWidth / maxHeight) {
if (w > maxWidth) {
h = (h * maxWidth) / w;
w = maxWidth;
}
} else {
if (h > maxHeight) {
w = (w * maxHeight) / h;
h = maxHeight;
}
}
}
let ctx = cvs.getContext(‘2d’);
let size = prepareCanvas(cvs, ctx, w, h, orientation);
// 填充白色背景
ctx.fillStyle = fillBgColor;
ctx.fillRect(0, 0, size.w, size.h);
// 将图片绘制到 Canvas 上,从原点 0,0 绘制到 w,h
ctx.drawImage(image, 0, 0, size.w, size.h);
let dataUrl = cvs.toDataURL(`image/${fileType}`, quality);
// 当图片大小 > maxSize 时,循环压缩, 并且循环不超过 5 次
let count = 0;
while (dataUrl.length > maxSize && count < 10) {
let imgDataLength = dataUrl.length;
let isDoubleSize = imgDataLength / maxSize > 2;
// 质量一次下降
quality -= isDoubleSize
? COMPRESS_QUALITY_STEP_BIG
: COMPRESS_QUALITY_STEP;
quality = parseFloat(quality.toFixed(2));
// 图片还太大的情况下,继续压缩。按比例缩放尺寸
let scaleStrength = COMPRESS_SIZE_RATE;
w = w * scaleStrength;
h = h * scaleStrength;
size = prepareCanvas(cvs, ctx, w, h, orientation);
// 将图片绘制到 Canvas 上,从原点 0,0 绘制到 w,h
ctx.drawImage(image, 0, 0, size.w, size.h);
console.log(`imgDataLength:${imgDataLength} , maxSize –> ${maxSize}`);
console.log(`size.w:${size.w}, size.h:${size.h}, quality:${quality}`);
dataUrl = cvs.toDataURL(`image/jpeg`, quality);
count++;
}
console.log(`imgDataLength:${dataUrl.length} , maxSize –> ${maxSize}`);
console.log(`size.w:${size.w}, size.h:${size.h}, quality:${quality}`);
cvs = ctx = null;
return dataUrl;
}
/**
* 准备画布
* cvs 画布
* ctx 上下文
* w : 想要画的宽度
* h : 想要画的高度
* orientation : 屏幕方向
*/
function prepareCanvas(cvs, ctx, w, h, orientation) {
cvs.width = w;
cvs.height = h;
// 判断图片方向,重置 canvas 大小,确定旋转角度,iphone 默认的是 home 键在右方的横屏拍摄方式
let degree = 0;
switch (orientation) {
case 3:
//iphone 横屏拍摄,此时 home 键在左侧
degree = 180;
w = -w;
h = -h;
break;
case 6:
//iphone 竖屏拍摄,此时 home 键在下方 (正常拿手机的方向)
cvs.width = h;
cvs.height = w;
degree = 90;
// w = w;
h = -h;
break;
case 8:
//iphone 竖屏拍摄,此时 home 键在上方
cvs.width = h;
cvs.height = w;
degree = 270;
w = -w;
// h = h;
break;
}
// console.log(`orientation –> ${orientation} , degree –> ${degree}`);
// console.log(`w –> ${w} , h –> ${h}`);
// 使用 canvas 旋转校正
ctx.rotate((degree * Math.PI) / 180);
return {w, h};
}
/**
* 截取‘data:image/jpeg;base64,’,
* 截取到第一个逗号
*/
export function removeBase64Header(content) {
if (content.substr(0, 10) === ‘data:image’) {
let splitIndex = content.indexOf(‘,’);
return content.substring(splitIndex + 1);
}
return content;
}
export function getFileType(fileName = ”) {
return fileName.substring(fileName.lastIndexOf(‘.’) + 1);
}
export function checkAccept(
file,
accept = ‘image/jpeg,image/jpg,image/png,image/gif’
) {
return accept.toLowerCase().indexOf(file.type.toLowerCase()) !== -1;
}
相关链接
个人博客代码片段