图片上传是前端中常见的的业务场景。无论是前台还是后台,适当的对图片进行压缩处理,可以显著的提升用户体验。而在后台管理系统中,图片压缩不仅仅能够提升后台管理员操作体验,更是可以防止后台设置过大的图片导致前台图片加载过久,从而影响用户体验。
之前我在网上冲浪的时候,经常会看到一些关于如何实现前端图片压缩的文章,基本大概的流程都是:
- input 读取到 image/file ,使用 FileReader 将其转换为 base64 编码
- 新建 img ,使其 src 指向刚刚的 base64
- 新建 canvas ,将 img 画到 canvas 上
- 利用 canvas.toDataURL/toBlob 将 canvas 导出为 base64 或 Blob
- 将 base64 或 Blob 转化为 File
基本的流程就是这样,也只能是这样了。实现图片压缩的关键是在 canvas.toDataURL/toBlob 这两个 API。在调用的时候可以传一个位于 0 ~ 1 之间的数字进去。而这个 0 ~ 1 之间的数字就是你想要压缩的图片质量,数值越小,压缩的图片的尺寸也会越小。
然而,实际上呢?
我在 Vue 里实现了一个方法,大概长这样:
Vue.prototype.$comp = function(file, quality) {
if (file[0]) {
return Promise.all(Array.from(file).map(e => Vue.prototype.$compression(e, quality))) // 如果是 file 数组返回 Promise 数组
} else {
return new Promise((resolve) => {
const reader = new FileReader() // 创建 FileReader
reader.onload = ({ target: { result: src }}) => {
const image = new Image() // 创建 img 元素
image.onload = async() => {
const canvas = document.createElement('canvas') // 创建 canvas 元素
canvas.width = image.width
canvas.height = image.height
canvas.getContext('2d').drawImage(image, 0, 0, image.width, image.height) // 绘制 canvas
const canvasURL = canvas.toDataURL('image/jpeg', quality)
const buffer = atob(canvasURL.split(',')[1])
let length = buffer.length
const bufferArray = new Uint8Array(new ArrayBuffer(length))
while (length--) {
bufferArray[length] = buffer.charCodeAt(length)
}
const miniFile = new File([bufferArray], file.name, { type: 'image/jpeg' })
resolve({
file: miniFile,
origin: file,
beforeSrc: src,
afterSrc: canvasURL,
beforeKB: Number((file.size / 1024).toFixed(2)),
afterKB: Number((miniFile.size / 1024).toFixed(2))
})
}
image.src = src
}
reader.readAsDataURL(file)
})
}
}
大家可别小瞧了这个代码,这可是可以拿优秀员工的代码的!!保存!!!
同时这里有一张网图:
此时在组件中调用压缩方法:
renderImg(files) { // 获取文件
console.log(files[0])
this.$comp(files[0], 0.9).then(res => console.log(res.file))
}
打印结果为:
是的!没错!压缩后的图片体积变大了,情况发生在当 quality 区间在 0.5 ~ 1 时,不同图片有所不同。
当然也有些图片压缩后的体积都是小于原体积的:
renderImg(files) { // 获取文件
console.log(files[0])
this.$comp(files[0], 0.999999).then(res => console.log(res.file))
}
结果是:
所以现在的结论是:
- 第一:canvas.toDataURL/toBlob 这两个 API确实可以实现图片压缩,但是压缩后的体积是不可控的,可能在一定的 quality 值外,图片反而会变大。
- 第二:quality 的大小和压缩后的图片体积没有一个通用的曲线函数可以描述。
第一点上面已经验证过了,下面来验证第二点:
下面会分别用几张不同大小的图片,分别从 quality 0.01 一直压缩到 0.99 ,已验证 quality 和压缩后的图片的关系:
首先是一张:
压缩曲线图为:
接下来是一张:
压缩曲线图为:
接下来是一张:
压缩曲线图为:
最后是一张:
压缩曲线图为:
以上曲线图,横轴是 quality,纵轴是当前 quality 压缩图片后图片的体积(单位:byte)。
虽然可以确定,没有一个常规曲线函数可以描述 quality 和压缩后图片体积的关系。但是从总体趋势来说,压缩后的图片体积和 quality 是成正比的。
而在实际的项目应用中,我们关心比较多的也是:如何把大图压缩到指定体积大小就好。而不是把图片压缩至自身的 1/2 或者 1/3 这样的。
所以,即使是压缩后的图片可能会大于原图。我们也不会太关心。我们只需要找到能让图片压缩到指定大小的 quality 就好。
那么怎么找呢?
鉴于上面的曲线图,我们没有办法根据压缩后的体积或 quality 反推出彼此。最笨的办法是像我实现这个曲线图一样,把 quality 从 0.01 一直试到 0.99。但是这样下来,可能需要 10s 左右。是的!十秒!执行一百次左右的压缩需要10s左右。单次压缩需要的耗时大约在 100ms ~ 130 ms 左右。当然这个很图片大小以及 quality 有关。
那么有没有一种快速的方式找到指定的 quality 呢?
使用二分法。
简单来说,就是我有一个 200KB 的图片,我想要压缩到 100KB。但是我不知道 quality 为多少时图片为 100KB。但是我知道 quality 越大图片就越大。因此,我先让 quality 为 0.5。此时压缩一次图片,并得到压缩后的体积。和 100KB 进行比较,如果大于 100KB ,表示 quality 大于 0.5,如果小于 100KB,表示 quality 小于 0.5。假设现在 quality 大于 0.5。那么就让 quality 变为 0.75。根据得出的结果判断 quality 是在 0.5~0.75之间,还是0.75~0.99之间。得到结果之后再让将 quality 设置为 0.625 或者 0.875...
以此类推,quality 的大小就能被定位在 1 / 2 的 n 次方这个范围。n 即二分的次数。
有了这个思路,我们做起来就很快了:
Vue.prototype.$compression = function(file, size = 20, device = 4) {
if (file[0]) {
return Promise.all(Array.from(file).map(e => Vue.prototype.$compression(e, size))) // 如果是 file 数组返回 Promise 数组
} else {
return new Promise((resolve) => {
const reader = new FileReader() // 创建 FileReader
reader.onload = ({ target: { result: src }}) => {
const fileSize = Number((file.size / 1024).toFixed(2))
if (fileSize <= size) {
resolve({ file: file, origin: file, beforeSrc: src, afterSrc: src, beforeKB: fileSize + 'KB', afterKB: fileSize + 'KB', detail: [], quality: null })
} else {
const image = new Image() // 创建 img 元素
image.onload = async() => {
const canvas = document.createElement('canvas') // 创建 canvas 元素
canvas.width = image.width
canvas.height = image.height
canvas.getContext('2d').drawImage(image, 0, 0, image.width, image.height) // 绘制 canvas
let canvasURL, miniFile
let L = true
let quality = 0
const detail = []
let start = Date.now()
for (let i = 1; i <= device; i++) {
canvasURL = canvas.toDataURL('image/jpeg', L ? (quality += 1 / (2 ** i)) : (quality -= 1 / (2 ** i)))
const buffer = atob(canvasURL.split(',')[1])
let length = buffer.length
const bufferArray = new Uint8Array(new ArrayBuffer(length))
while (length--) {
bufferArray[length] = buffer.charCodeAt(length)
}
miniFile = new File([bufferArray], file.name, { type: 'image/jpeg' });
(miniFile.size / 1024) > size ? L = false : L = true
detail.push({
quality,
size: miniFile.size,
kb: Number((miniFile.size / 1024).toFixed(2)),
time: Date.now() - start
})
start = Date.now()
}
resolve({
detail,
quality,
file: miniFile,
origin: file,
beforeSrc: src,
afterSrc: canvasURL,
beforeKB: Number((file.size / 1024).toFixed(2)),
afterKB: Number((miniFile.size / 1024).toFixed(2))
})
}
image.src = src
}
}
reader.readAsDataURL(file)
})
}
}
这就是使用二分法将图片压缩至指定体积的方法。二分的次数默认为 4 次。执行时长为 400ms ~ 500ms。还算可以接受,不过因为二分精度的原因,不能保证图片一定会精准的压缩至指定体积,可能会有一定误差。如果要适当提高精度,可以增加二分次数,但是这又会使得图片压缩时间过长。我在上面的方法返回的对象里,记录了每次压缩的效果的时长,这里有个简单的例子:
上面的压缩结果还算符合预期。再来一个:
这个就有点偏差了,改下二分次数试试:
将 4 次二分改为 6 次。实际压缩结果更接近期望结果。但是执行时长却多了 100多 ms。
DEMO体验地址
以上就是本人在后台管理系统项目中遇到的图片压缩问题以及解决方案。虽然有些粗糙,但是勉强能实现需求。当然这只是后台管理系统的实现,仅适用于P端。如果是移动端,还要注意处理一些边界情况,如 IOS 尺寸限制,png 透明图变黑等。
码字不易,希望多多支持。如果你有什么更好的想法或者发现了BUG,还请不吝赐教,万分感谢。
————————————————
版权声明:本文为CSDN博主「树林同学」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_34183908/article/details/112491598
前端图片最优化压缩方案 - 掘金前端最优化压缩图片上传方案!!!再次优化升级!14M压缩后仅剩130KB。 图片上传是前端中常见的的业务场景。无论是前台还是后台,适当的对图片进行压缩处理, 可以显著的提升用户体验。https://juejin.cn/post/6940430496128040967
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input id="fileInput" type="file" />
<img id="img" src="" alt="">
</body>
<script>
let fileId = document.getElementById('fileInput')
let img = document.getElementById('img')
fileId.onchange = function (e) {
let file = e.target.files[0]
compressImg(file, 0.92).then(res => {//compressImg方法见附录
console.log(res)
img.src = window.URL.createObjectURL(res.file);
})
}
/**
* 压缩方法
* @param {string} file 文件
* @param {Number} quality 0~1之间
*/
function compressImg(file, quality) {
if (file[0]) {
return Promise.all(Array.from(file).map(e => compressImg(e,
quality))) // 如果是 file 数组返回 Promise 数组
} else {
return new Promise((resolve) => {
const reader = new FileReader() // 创建 FileReader
reader.onload = ({
target: {
result: src
}
}) => {
const image = new Image() // 创建 img 元素
image.onload = async () => {
const canvas = document.createElement('canvas') // 创建 canvas 元素
canvas.width = image.width
canvas.height = image.height
canvas.getContext('2d').drawImage(image, 0, 0, image.width, image.height) // 绘制 canvas
const canvasURL = canvas.toDataURL('image/jpeg', quality)
const buffer = atob(canvasURL.split(',')[1])
let length = buffer.length
const bufferArray = new Uint8Array(new ArrayBuffer(length))
while (length--) {
bufferArray[length] = buffer.charCodeAt(length)
}
const miniFile = new File([bufferArray], file.name, {
type: 'image/jpeg'
})
resolve({
file: miniFile,
origin: file,
beforeSrc: src,
afterSrc: canvasURL,
beforeKB: Number((file.size / 1024).toFixed(2)),
afterKB: Number((miniFile.size / 1024).toFixed(2))
})
}
image.src = src
}
reader.readAsDataURL(file)
})
}
}
</script>
</html>
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)