隨着手機攝像機像素的提高,動不動就要拍月亮做微焦整3D搞VR,圖片視頻的壓縮技術已經越來越被重視。因爲項目需要隨即看了幾天資料,自己動手封裝了一個前端壓縮圖片的方法。
預覽下效果先
壓縮前
壓縮後
第一步:
- 理解前端壓縮的原理,理解下來發現吧前端壓縮實際上是個高損壓縮!無非是利用canvas重繪圖片降低像素和質量。網上有更好的tingPng tingJPEG更高大上!!可是奈何我實力有限,物質匱乏,即不能理解借鑑模仿其算法,亦囊中羞澀捨不得幾張鈔票,只好退而求其次用個高損方法擼一擼罷了。
第二步:
- 兼容移動端,一番考察發現ios竟然對canvas支持不充分,不允許高像素圖片繪製!!還有圖片旋轉倒橫各種問題需要解決!!
- 欲用算法腦不夠,將用畫布兼容爛,前端難,前端難,搞後臺,也很難!
- 頭頂黑髮漸蕭索,臉滾鍵盤寫插件。
第三步:
- 插件能幹嘛:對jpg/jpeg/png圖片進行高效壓縮,支持多張圖片同時壓縮,支持移動端圖片壓縮,可校正圖片角度。
第四步:
- 封裝插件(df_compressImg.js)
/**
*
* 作者:Z_D_F
*
* 郵箱地址:[email protected]
*
* 日期: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>