前端指定大小缩放比例压缩图片
核心内容
- 图片压缩,包括大小和缩放
- 瓦片绘制,针对 ios 图片的大小超过两百万像素,图片无法绘制到canvas上的问题
- canvas的大小限制,如果canvas的大小大于大概五百万像素(即宽高乘积)的时候,不仅图片画不出来,其他什么东西也都是画不出来的。【解决:对图片的宽高进行适当压缩】
- canvas的
toDataURL
和toBlob
是只能压缩图片格式为image/jpeg
或者image/webp
的图片,需要将图片类型设置为image/jepg
- png类型图片,写入canvas后背景会变成黑色,手动为图片铺白色底
- 由于无法直接设置图片大小,所以通过二分法尽可能找到最接近设置大小的值
效果预览,如下是图片设置最大10KB,缩放比例0.5
compress(file, 10, 0.5);
您可以点击此处进行测试测试链接
准备知识
在移动端压缩图片并且上传主要涉及到 filereader 、canvas 以及 formdata 这三个h5的api。同时还包括一些blob、base64、blobURL等相关转换的知识。
通过本文你将会了解:
- blob => dataURL(base64)
FileReader.toBlob()
- blob => blobURL
URL.createObjectURL
、URL.revokeObjectURL
canvas
绘制图片ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
canvas
瓦片绘制- 二分法求最接近值
- 循环的异步执行
async await
(此方法并非压缩方法需要,只在demo中用到) canvas
压缩图片canvas.toDataURL(type, encoderOptions)
、canvas.toBlob(callback, type, encoderOptions)
相关内容下文代码中会体现,如果希望详细了解也可以参考blob、base64、file相互转换;
实现
canvas
为本例的核心部分,图片质量,压缩比例均需要通过canvas
实现.
canvas
具有图像操作能力,支持将一个已有的图片作为图片源,来操作图像。
设计
方法接收 file 类型文件,压缩大小,压缩比例,返回压缩后的文件
/**
* @param: (file: blob, maxSize: number, size: number[0-1])
* @return: {blob, base64}
*/
function compress(file, maxSize, scale): Promise<> {
...
}
1. 接收file,并转为url(base64/objectURL)传入img, 在回调函数中处理压缩逻辑
// 利用 FileReader 将blob转base64
// function blob2base64(blob) {
// return new Promise(function(resolve, reject) {
// const reader = new FileReader();
// reader.onload = (e) => {
// resolve(e.target.result)
// }
// reader.readAsDataURL(blob)
// })
// }
// const dataUrl = blob2base64(file);
// 利用URL.createObjectURL 将blob转为objectURL
const blobUrl = URL.createObjectURL(file);
const tmpImg = new Image();
tmpImg.onLoad = function(e) {
const img = e.target;
URL.revokeObjectURL(file); // 清理图片缓存
// ... 压缩代码
}
tmpImg.src = blobUrl;
2. 绘制canvas
/**
* @description 将图片绘制到画布上,并根据缩放比例进行缩放,若图片大于2000000像素则进行瓦片处理
* @param {context: canvasContext, img: imgObj, scale: 0-1}
*/
function drawImage(context, img, scale) {
const {
width: sourceWidth,
height: sourceHeight,
} = img;
const pSize = sourceWidth * sourceHeight;
const targetWidth = sourceWidth * scale,
targetHeight = sourceHeight * scale;
const defSize = 1000; // 设置瓦片默认宽高
const cols = pSize > 2e6 ? ~~(targetWidth / defSize) + 1 : 1; // 列数
const rows = pSize > 2e6 ? ~~(targetHeight / defSize) + 1 : 1; // 行数
// 瓦片绘图
for (let i = 0; i < rows; i ++) { // 遍历列
for (let j = 0; j < cols; j ++) { // 遍历行
const canvasWidth = j === (cols - 1) ? targetWidth % defSize : defSize;
const canvasHeight = i === (rows - 1) ? targetHeight % defSize : defSize;
const canvasLeft = j * defSize;
const canvasTop = i * defSize;
const imgWidth = canvasWidth / scale;
const imgHeight = canvasHeight / scale;
const imgLeft = canvasLeft / scale;
const imgTop = canvasTop / scale;
context.drawImage(img, imgLeft, imgTop, imgWidth, imgHeight, canvasLeft, canvasTop, canvasWidth, canvasHeight);
}
}
}
注意:demo中添加了定时器,延迟每次绘制150ms执行,并且defSize设置为50,用来展示瓦片绘制的效果,实际上并不需要
demo 代码
function drawImage(context, img, scale) {
const {
width: sourceWidth,
height: sourceHeight,
} = img;
const targetWidth = sourceWidth * scale,
targetHeight = sourceHeight * scale;
// 定义sleep方法用于延迟执行
function sleep(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, time * 120)
})
}
return new Promise(async (resolve, reject) => {
const defSize = 50;
const cols = ~~(targetWidth / defSize) + 1; // 列数
const rows = ~~(targetHeight / defSize) + 1; // 行数
// 这里注意,for循环若想要异步执行,则方法需要加上 async 关键字,并且异步方法通过 await 调用
for (let i = 0; i < rows; i ++) { // 遍历列
for (let j = 0; j < cols; j ++) { // 遍历行
const canvasWidth = j === (cols - 1) ? targetWidth % defSize : defSize;
const canvasHeight = i === (rows - 1) ? targetHeight % defSize : defSize;
const canvasLeft = j * defSize;
const canvasTop = i * defSize;
const imgWidth = canvasWidth / scale;
const imgHeight = canvasHeight / scale;
const imgLeft = canvasLeft / scale;
const imgTop = canvasTop / scale;
await sleep(1);
context.drawImage(img, imgLeft, imgTop, imgWidth, imgHeight, canvasLeft, canvasTop, canvasWidth, canvasHeight);
}
}
resolve('success');
})
}
上述代码涉及到循环的异步执行,参考注释,另外,这种方式在 forEach
方法中无效,因为 forEach
本身为同步设计的,不支持异步
3. 压缩方法
/**
* @description 该步骤为关键步骤,通过canvas的toDataURL和toBlob对图片进行压缩
* @param {canvas: Canvas, quality: 0-1 图片质量}
*/
function compressBlob(canvas, quality) {
const type = "image/jpeg"; // 只有jpeg格式图片支持图片质量压缩
const dataURL = canvas.toDataURL(type, quality);
return new Promise((resolve, reject) => {
canvas.toBlob(function(blob) {
resolve({
dataURL,
blob,
});
}, type, quality);
})
}
4. 压缩算法,计算最接近maxSize的文件大小
这一步利用二分法获取合适的 quality
, 重复调用 compressBlob()
, 使得文件大小尽量接近设定值
/**
* @param {canvas: Canvas, maxSize: number}
*/
function compressCaculation(canvas, maxSize) {
let count = 1; // 记录循环次数,防止死循环
let quality = 1; // 图片质量
let maxQuality = 1, minQuality = 0; // 定义图片质量范围
return new Promise(function(resolve, reject) {
async function fn() {
quality = (maxQuality + minQuality) / 2;
// 获取压缩后结果
let res = await compressBlob(canvas, quality);
const { blob, dataURL } = res;
const { size } = blob;
console.log(`第${count}次压缩:`, quality, size);
// 二分法获取最接近值
function partition() {
count ++;
if (size > maxSize) {
maxQuality = quality;
} else {
minQuality = quality;
}
fn();
}
if(count < 10 && size !== maxSize) { // 默认循环不大于20次,认为10次内的结果已足够接近
partition();
} else if(size > maxSize) { // 若限制次数之后size > maxSize,则继续执行,至结果小于maxSize
partition();
} else {
resolve(res);
}
}
fn();
})
}
5. 整合方法
串联整个压缩流程
/**
*
* @param {file: File|Blob, maxSize: number, scale: 0-1}
* @return {name: string, blob: Blob, dataURL: base64}
*/
function compress(file, maxSize = 13, scale = 0.5) {
maxSize *= 1024; // maxSize 单位为kb,这里转为b
const {
size: sourceSize,
name,
} = file;
const fileType = name.replace(/^.*\.(\w*)$/, '$1').toLowerCase();
if (!['jpg', 'jpeg', 'png', 'webp'].includes(fileType)) return Promise.reject('文件格式不支持!');
const blobUrl = URL.createObjectURL(file);
const tmpImg = new Image();
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
return new Promise((resolve, reject) => {
tmpImg.onload = async function(e) {
const img = e.target;
URL.revokeObjectURL(file); // 清理图片缓存
let { width, height } = img;
width *= scale;
height *= scale;
//如果图片大于四百万像素,重写缩放比例比并将大小压至400万以下
if ((width * height / 4e6) > 1) {
scale = 4e6 / (width * height);
width *= scale;
height *= scale;
}
// 设置画布大小
canvas.width = width;
canvas.height = height;
context.fillStyle = "#fff"; // 填充底色,处理png背景为黑色的问题
context.fillRect(0, 0, width, height);
// canvas 绘图,这一步将图片按比例缩放,并处理瓦片问题
drawImage(context, img, scale);
compressCaculation(canvas, maxSize).then(res => {
console.log('压缩完成,压缩前:', sourceSize, ' => 压缩后:', res.blob.size);
resolve({
name,
...res,
})
})
}
tmpImg.src = blobUrl;
})
}
测试
<input type="file" onchange="upload(this)" />
function upload(e) {
[file] = e.files;
// 最大值10M, 缩放比例0.5
compress(file, 10, 0.5).then(res => {
console.log(res);
});
}
效果如下
参考链接
转载自:https://juejin.cn/post/7039690969360367630