纯前端将图片压缩至指定大小

html 部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<!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>
<div id="app"> <input type="file" @change="confirm" /> </div>
<script src="./canvas.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!',
state: {
fileObjs: [], // item { originFile, compressBase64, compressFile }
},
},
methods: {
confirm(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, 2048).then((res) => {
this.compressCallBack(file, fileObj, res)
}, (err) => { // 压缩失败,则返回原图片的信息
this.compressCallBack(file, fileObj, err)
})
}),
this.setState({
fileObjs: [...fileObjs]
})
e.target.value = ''
},
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)) {
const link = document.createElement('a')
let blob = new Blob([fileObj.compressFile], {
type: "image/png"
})
link.style.display = 'none'
link.href = URL.createObjectURL(blob)
link.setAttribute('download', '列表' + '.png')
document.body
.appendChild(link)
link.click()
document.body.removeChild(link)
}
},
},
})
</script>
</body>

</html>

js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// 将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)
)

// originfile 文件,maxSize 图片指定最大尺寸
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 })
})