随着手机摄像机像素的提高,动不动就要拍月亮做微焦整3D搞VR,图片视频的压缩技术已经越来越被重视。因为项目需要随即看了几天资料,自己动手封装了一个前端压缩图片的方法。
预览下效果先
压缩前
image.png
压缩后
image.png
第一步:
- 理解前端压缩的原理,理解下来发现吧前端压缩实际上是个高损压缩!无非是利用canvas重绘图片降低像素和质量。网上有更好的tingPng tingJPEG更高大上!!可是奈何我实力有限,物质匮乏,即不能理解借鉴模仿其算法,亦囊中羞涩舍不得几张钞票,只好退而求其次用个高损方法撸一撸罢了。
第二步:
- 兼容移动端,一番考察发现ios竟然对canvas支持不充分,不允许高像素图片绘制!!还有图片旋转倒横各种问题需要解决!!
- 欲用算法脑不够,将用画布兼容烂,前端难,前端难,搞后台,也很难!
- 头顶黑发渐萧索,脸滚键盘写插件。
第三步:
- 插件能干嘛:对jpg/jpeg/png图片进行高效压缩,支持多张图片同时压缩,支持移动端图片压缩,可校正图片角度。
第四步:
- 封装插件(df_compressImg.js)
/**
*
* 作者:Z_D_F
*
* 邮箱地址:1564950920@qq.com
*
* 日期:2020-7-28
*
* 插件功能:前端图片压缩,可传单张或者多张图片数据,可选择返回图片类型,可手动选择压缩率(0-1)可校正图片倾斜度。
*
* 兼容PC移动端(H5)
*
*
*
* 原理:
*
* 基于canvas重绘并生成新的图片,实现有损的前端压缩,可压缩png/jpeg/jpg格式的图片,压缩率可选(0.1-1),推荐0.5(可保证一定质量的低损压缩),无惧质量比如头像图片推荐0.2,可实现压缩效率。
*
* 针对移动端图片的图片上传进行了细致的处理,例如角度调整(EXIF),超大型图片的处理。
*
*/
;(function (global, undefined) {
"use strict" //使用js严格模式检查,使语法更规范
let _global;
/**
*
* @param {fileArray} files 需要压缩的图片文件集合
* @param {Function name(params) {}} getBlobList 钩子函数返回压缩后的文件集合
* @param {Number} quality 压缩率
* @param {String} returnType
* @returns
*/
function dfCompressImage(fileList, getBlobList, quality, returnType) {
let blobList = []; //压缩后的二进制图片数据列表
let qlty = quality || 0.2; //图片压缩品质,默认是0.2,可选范围是0-1的数字类型的值,可配置
let rType = returnType || "bold"; // 返回压缩后图片的类型,默认二进制流,可选base64。
// 判断参数fileList的长度是否大于0
if (!fileList) {
console.error('警告:参数fileList不能为空!!!')
return;
}
// 判断参数fileList的长度是否大于0
if (!fileList.length) {
console.error('警告:传进方法process的参数fileList长度必须大于零!!!')
return;
}
// 把传进来的fileList转为数组类型
let files = Array.prototype.slice.call(fileList);
// console.log(fileList, "fileList")
return new Promise((resolve, reject) => {
files.forEach((file, i) => {
if (!/\/(?:jpeg|png)/i.test(file.type)) {
console.error('警告:图片必须是jpeg||png类型!!!');
return;
}
// 处理压缩图片
readerFile(file, qlty, rType, blobList, getBlobList, fileList);
})
if (!getBlobList) {
resolve(blobList);
}
});
}
function readerFile(file, qlty, rType, blobList, getBlobList, fileList) {
let reader = new FileReader();
reader.onload = function () {
let image = new Image();
image.src = this.result;
let canvas = document.createElement("canvas"); //用于压缩图片(纠正图片方向)的canvas
let context = canvas.getContext('2d');
// 图片加载完毕之后进行压缩,否则获取不到你的图片宽高。
if (image.complete) {
callback();
} else {
image.onload = callback;
}
function callback() {
// 这一步很关键
let imgWidth = image.width;
let imgHeight = image.height;
var originWidth = image.width
var originHeight = image.height
if (1 * 1024 <= parseInt((file.size / 1024).toFixed(2)) && parseInt((file.size / 1024).toFixed(2)) <= 10 * 1024) {
var maxWidth = 1600
var maxHeight = 1600
imgWidth = originWidth
imgHeight = originHeight
// 图片尺寸超过的限制
if (originWidth > maxWidth || originHeight > maxHeight) {
if (originWidth / originHeight > maxWidth / maxHeight) {
// 更宽,按照宽度限定尺寸
imgWidth = maxWidth
imgHeight = Math.round(maxWidth * (originHeight / originWidth))
} else {
imgHeight = maxHeight
imgWidth = Math.round(maxHeight * (originWidth / originHeight))
}
}
}
if (10 * 1024 <= parseInt((file.size / 1024).toFixed(2)) && parseInt((file.size / 1024).toFixed(2)) <= 20 * 1024) {
maxWidth = 1400
maxHeight = 1400
imgWidth = originWidth
imgHeight = originHeight
// 图片尺寸超过的限制
if (originWidth > maxWidth || originHeight > maxHeight) {
if (originWidth / originHeight > maxWidth / maxHeight) {
// 更宽,按照宽度限定尺寸
imgWidth = maxWidth
imgHeight = Math.round(maxWidth * (originHeight / originWidth))
} else {
imgHeight = maxHeight
imgWidth = Math.round(maxHeight * (originWidth / originHeight))
}
}
}
let Orientation = ''; //图片方向角
// 角度判断(如果角度不对就纠正)
EXIF.getData(image, function () {
EXIF.getAllTags(image);
Orientation = EXIF.getTag(image, 'Orientation');
if (Orientation == "" || Orientation == undefined || Orientation == null) {
Orientation = 1;
}
});
if (Orientation && Orientation != 1) {
switch (Orientation) {
case 6: // 需要顺时针90度旋转
canvas.width = imgHeight;
canvas.height = imgWidth;
context.fillStyle = "#fff"; // 铺底色
context.fillRect(0, 0, width, height);
context.rotate(Math.PI / 2);
context.clearRect(0, 0, imgWidth, imgHeight)
context.drawImage(image, 0, -imgHeight, imgWidth, imgHeight);
break;
case 3: // 需要180度旋转
context.fillStyle = "#fff"; // 铺底色
context.rotate(Math.PI);
context.clearRect(0, 0, imgWidth, imgHeight)
context.drawImage(image, -imgWidth, -imgHeight, imgWidth, imgHeight);
break;
case 8: // 需要逆时针90度旋转
canvas.width = imgHeight;
canvas.height = imgWidth;
context.fillStyle = "#fff"; // 铺底色
context.rotate(3 * Math.PI / 2);
context.clearRect(0, 0, imgWidth, imgHeight)
context.drawImage(image, -imgWidth, 0, imgWidth, imgHeight);
break;
}
} else {
canvas.width = imgWidth
canvas.height = imgHeight
context.fillStyle = "#fff"; // 铺底色
context.clearRect(0, 0, imgWidth, imgHeight)
context.drawImage(image, 0, 0, imgWidth, imgHeight);
}
// 获取图片压缩前大小,打印图片压缩前大小
let size = file.size / 1024 > 1024 ? (~~(10 * file.size / 1024 / 1024)) / 10 + "MB" : ~~(file.size / 1024);
let boldFile, canvasURL;
if (size < 400) {
blobList.push(file);
} else {
//获取压缩后的图片二进制数据
canvasURL = canvas.toDataURL('image/jpeg', qlty)
const buffer = atob(canvasURL.split(',')[1])
let length = buffer.length
const bufferArray = new Uint8Array(new ArrayBuffer(length))
while (length--) {
bufferArray[length] = buffer.charCodeAt(length)
}
boldFile = new File([bufferArray], file.name, {
type: 'image/jpeg'
})
// let boldFile = file.type == "image/jpeg" ? canvasURL : dataURItoBlob(canvasURL);
// boldFile = dataURItoBlob(canvasURL);
blobList.push(boldFile);
}
//将压缩后的二进制图片数据对象(blob)组成的list通过钩子函数(getBlobList)返回出去
if (blobList.length === fileList.length) {
if (getBlobList){
getBlobList(blobList);
}
}
//清除canvas画布的宽高
canvas.width = canvas.height = 0;
// 清除image对象
image = null;
}
};
reader.readAsDataURL(file);
}
// function dfCompressImage(){}
// dfCompressImage.prototype.compress = compress
// 最后将插件对象暴露给全局对象
_global = (function () {
return this || (0, eval)('this');
}());
if (typeof module !== "undefined" && module.exports) {
module.exports = dfCompressImage;
} else if (typeof define === "function" && define.amd) {
define(function () {
return dfCompressImage;
});
} else {
!('dfCompressImage' in _global) && (_global.dfCompressImage = dfCompressImage);
}
}());
依赖
- 依赖于exif.js,可自行百度谷歌获取资源,我放个链接可能以后就失效了。
使用
- 建立文件夹df_demo,新建js文件夹,放入exif.js和上面的df_compressImg.js,新建index.html
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
<title>图片压缩</title>
<style>
* {
margin: 0;
padding: 0;
}
.image-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
li {
list-style-type: none;
}
a,
input {
outline: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
#fileInput {
display: none;
}
.title {
padding: 10px 20px;
font-size: 16px;
font-weight: bold;
}
canvas {
width: 100%;
border: 1px solid #000000;
}
#addImg {
display: block;
margin: 10px;
height: 40px;
width: 200px;
text-align: center;
line-height: 40px;
border: 1px solid;
border-radius: 5px;
cursor: pointer;
}
.img_list,
.imgCompress_list {
width: calc(100% - 42px);
max-width: 980px;
min-height: 100px;
margin: 10px 20px;
border: 1px solid black;
border-radius: 5px;
padding: 10px;
}
.img_list li,
.imgCompress_list li {
position: relative;
display: inline-block;
width: 100px;
height: 100px;
margin: 5px 5px 10px 5px;
border: 1px solid rgb(100, 149, 198);
background: #fff no-repeat center;
background-size: cover;
}
.progress {
position: absolute;
width: 100%;
height: 20px;
line-height: 20px;
bottom: 0;
left: 0;
background-color: rgba(100, 149, 198, .5);
}
.progress span {
display: block;
width: 0;
height: 100%;
background-color: rgb(100, 149, 198);
text-align: center;
color: #FFF;
font-size: 13px;
}
.size {
position: absolute;
width: 100%;
height: 15px;
line-height: 15px;
bottom: -18px;
text-align: center;
font-size: 13px;
color: #666;
}
.tips {
display: block;
text-align: center;
font-size: 13px;
margin: 10px;
color: #999;
}
</style>
</head>
<body>
<div class="image-content">
<input type="file" id="fileInput" capture="camera" accept="image/*" multiple>
<p class="title">压缩前</p>
<ul class="img_list">
</ul>
<a id="addImg">选择压缩图片</a>
<span class="tips">只允许上传jpg、png</span>
<p class="title">压缩后</p>
<ul class="imgCompress_list">
</ul>
</div>
<!-- df_compressImg是压缩图片插件,exif.js是矫正图片方向的插件,前者是依赖后者的需要一起引入且需要后者在前面引入 -->
<script src="js/exif.js"></script>
<script src="js/df_compressImg.js"></script>
<script type="text/javascript">
var fileInputEle = document.getElementById("fileInput");
//用来存储压缩后的图片二进制数据
var blobFileList;
//点击添加图片
document.getElementById('addImg').onclick = function () {
fileInputEle.click();
}
// 监听上传组件input的onchange事件,压缩图片
fileInputEle.onchange = function () {
var fileList = this.files;
// console.log('fileList:', fileList);
//预览压缩前的图片
var files = Array.prototype.slice.call(fileList);
files.forEach(function (file, i) {
var reader = new FileReader();
reader.onload = function () {
var li = document.createElement("LI")
li.style.backgroundImage = 'url(' + this.result + ')';
document.querySelector('.img_list').appendChild(li)
}
reader.readAsDataURL(file);
});
// 调用压缩方法传入钩子函数
// dfCompressImage(this.files, getBlobList, 0.2);
// 直接使用链式调用
dfCompressImage(this.files, false, 0.2).then(res => {
getBlobList(res)
});
}
// 钩子函数
function getBlobList(fileList) {
fileList.forEach(function (blob) {
var reader = new FileReader();
reader.onload = function () {
var li = document.createElement("LI")
li.style.backgroundImage = 'url(' + this.result + ')';
document.querySelector('.imgCompress_list').appendChild(li)
}
reader.readAsDataURL(blob);
})
}
</script>
</body>
</html>
网友评论