如何用 JavaScript 制作一个好用又好玩的图片压缩工具
时间:2022-12-12 13:28 作者:独元殇 分类: 前端技术
[toc]
前言
现在的设备发达了,图片拍下来动辄 5MB 10MB,单反相机歘欻欻一张经能达到 40MB,手机的内部储存也跟着很大,随便一个手机都 100G 。
但对于我来讲,反而不舒服。一张照片,占用 5mb 10mb 的空间真的合适吗?不舒服不是因为居安思危,杞人忧天,觉得体积大未来会把地球憋爆炸,而是觉得一张图片可以比文字占得空间大点,但 5mb 10mb 着实不配它占。它不配。
文字,真的太节约体积了,余华花很久写个《活着》,全书保存成 GBK 编码也不过占用 500kb,一张图片,用那么多体积确实不合算。大概看个样子就行了,只有极少数像素会经过中枢神经前额叶意识区域的处理。
另一方面,体积大了,在本地还行,硬盘上千个 GB,不碍事,但在服务器上,网页上,体积小的需求还是挺大,要不然谷歌也不会研制 webp 什么格式,emlog、七牛、阿里云也不会刻意考虑为图片压缩尺寸等措施。
所以,在适当情况下,如果图片能压缩到一定程度,确实是网络从业者的福音。尤其对我这种,以前特别害怕在网站上传图片,因为即使是 CDN ,也是花钱的,当然钱是小事,5 块 10 块够我用好几年,主要是如果一张图片 5M 的话,到时候七牛云倒闭,迁移资源时,工程量可大了!如果一张图片就几十 KB 的话,加起来那么多图也就几十 mB ,几百 mb,简洁可爱!(就像七八年前的 微信 一样,可惜物是人非今不如昔)
当然,图片压缩从来不是卡脖子的技术,微信、各种 APP 、PS 都能灵活的压缩图片,甚至 AI。即使懒得下载,点击,直接打开万能的互联网浏览器,搜索在线压缩图片,也能,不过还是效率不够行,不够方便。
中文互联网真的,处处都是注册、、、而且没啥技术含量,纯粹抄袭的别人的东西。为了更自由,我决定自己做一个,使用 JavaScript。而且使用的都是浏览器自带的 API ,什么 canvas API ,blob API....
功夫不负有心人,花了一傍晚的时间,我做出来了。
最终成果
就是这个链接。 https://www.ccgxk.com/249.html 【导航】---【小工具】---【图片超级压缩】。
由图可见,这种压缩效率还是很厉害的,虽然原图才 几十几百 kb,但如果原图是 5M 10M 也是可以压缩到 20 --- 30 kb 的。说实话 20 kb 的图,虽然模糊点,但足够把很多信息传递明白了。
其实,这个主要是有文字,模糊起来会看不清。如果是「风景图片」的话,越模糊,越有意境哈哈。
代码的话,还是花了很多功夫的。不一段一段讲了,先直接上最终的 html + javascript 。
<style>
.c {
margin-top: 20px;
margin-inline: auto;
}
i {
color:#c9c9c9;
}
.e2 {
background: aliceblue;
border: 0px;
}
.markdown {
text-align: inherit;
}
</style>
<div class="c" >
复制图片,在下面蓝框中粘贴,会自动按照下面设置的规则来压缩图片体积
<br><br>
<i>注意,直接鼠标复制处理后的图片,其体积会增长一部分(因浏览器本身特性),获取真实压缩图片应单击「下载最终结果」。</i>
<br><br>
<textarea class="e2" style="width: 100%;" rows="2" id="output"></textarea>
<br><br>
<button id="img_download" onclick="base64ToFile(out_base64, 'download.jpeg')">下载最终结果</button>(<span id="img_size"></span>):
<p id="imga">
<img id="testimg" src="" alt="" />
</p>
</div>
<fieldset style="width: 230px">
<legend>压缩规则</legend>
最大宽度 (px)<input type="number" id="in_maxwidth" onchange="re_config()" value="400" /><br><br>
质量 (1 - 10)<input type="range" name="points" min="1" max="100" id="in_quality" onchange="re_config()" value="50" /><span id="in_q_msg">5</span><br><br>
是否黑白化<input type="checkbox" id="in_balck" onchange="re_config()" checked /><br>
</fieldset>
<script>
/* 配置区 */
let drawWidth = 400; // 统一宽度值
let imgQuality = 0.5; // 质量
let is_balck = true; // 黑白
// ----------------
const c=document.createElement("canvas");
const ctx=c.getContext("2d");
let domImg;
let s_imgSize;
let r_imgSize;
let base64data;
let out_base64;
/* 程序入口 */
function drawimg(base64data){
creatDomImg(base64data);
setTimeout(function(){canvdraw()}, 1000);
}
/* 把图片弄到 domImg 中 */
function creatDomImg(base64data){
s_imgSize = parseInt(base64data.length / 1024 * 0.75) + "kb";
domImg = document.createElement("img");
domImg.src = base64data;
}
function canvdraw(){
/* 计算画布的宽高值 */
let scale = domImg.height / domImg.width;
let domImg_w = (domImg.width > drawWidth) ? drawWidth : domImg.width;
let domImg_h = (domImg.width > drawWidth) ? drawWidth * scale : domImg.height;
/* 画布生成 */
c.width = domImg_w;
c.height = domImg_h;
/* 在画布画图 */
ctx.drawImage(domImg, 0, 0, domImg_w, domImg_h);
/* 黑白化 */
if(is_balck){
const imgArrData = ctx.getImageData(0, 0, domImg_w, domImg_h);
for (let i = 0; i < imgArrData.data.length; i += 4) {
let r = imgArrData.data[i],
g = imgArrData.data[i + 1],
b = imgArrData.data[i + 2];
const avg = (r + g + b) / 3;
imgArrData.data[i] = imgArrData.data[i + 1] = imgArrData.data[i + 2] = avg;
}
ctx.putImageData(imgArrData, 0, 0);
}
/* 图片展示 */
out_base64 = c.toDataURL('image/jpeg', imgQuality);
testImg = document.getElementById("testimg");
testImg.src = out_base64;
/* 处理后的大小 */
r_imgSize = parseInt(out_base64.length / 1024 * 0.75) + "kb";
img_size.innerHTML = s_imgSize + " -> " + r_imgSize;
}
/* 粘贴事件后:获取粘贴图片,把 base64 数据扔给 drawimg() */
document.getElementById("output").addEventListener("paste", function (e) {
if ( !(e.clipboardData && e.clipboardData.items) ) return
var pasteData = e.clipboardData || window.clipboardData
pasteAnalyseResult = new Array
for(var i = 0; i < pasteData.items.length; i++) {
var item = pasteData.items[i]
if((item.kind == "file") && (item.type.match('^image/'))){
let imgData = item.getAsFile();
if (imgData.size === 0) return;
let reader = new FileReader();
reader.readAsDataURL(imgData);
reader.onload = function(){
base64data = this.result;
drawimg(base64data); // 获得图片 base64 数据,开始处理
}
break;
};
}
}, false);
/* 用户在界面自定义配置 */
function re_config(){
drawWidth = in_maxwidth.value;
imgQuality = Math.floor(in_quality.value) / 100;
in_q_msg.innerHTML = (imgQuality * 10).toString().match(/^\d+(?:\.\d{0,1})?/);
is_balck = in_balck.checked;
if(typeof base64data === 'undefined') return
img_size.innerHTML = "处理中...";
drawimg(base64data);
}
/* 下载 */
function base64ToFile(base,fileName) {
console.log(base)
if(typeof base === 'undefined') return
const arr = base.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
if (window.navigator.msSaveBlob) {
// for ie 10 and later
try {
const blobObject = new Blob([u8arr], { type: mime });
window.navigator.msSaveBlob(blobObject, 'aaa.xls');
} catch (e) {
console.log(e);
}
} else {
const url = window.URL.createObjectURL(new Blob([u8arr], { type: mime }));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link); // 下载完成移除元素
window.URL.revokeObjectURL(url); // 释放掉blob对象
}
}
</script>
代码讲解
获取粘贴板图片 base64
获取剪切板的程序,是固定写法,那么一大群 addEventListener ,复制粘贴就行。
最终获取剪切板里图片的 base64 ,放到 drawimg(base64data);
里。
虚拟 < img > 放图片
然后就要过流水线了。先创建一个虚拟的 DOM < img > 放内存里,(创建实体 也行,但没必要)。然后图片 src 就是这个 base64,这样,就有了这个 img 元素了。
为什么创建 img ,因为目前我只知道 < canvas > 画照片的办法,就是得有 < img > 才行。然后按照 api 方式,画图就行。
间隔 1 s 向 < canvas > 放图片
不过很可惜,这两个不能同时进行,创建了 < img >,还得等一段时间,可能这是单进程吧,所以我写了个延迟函数 setTimeout(function(){canvdraw()}, 1000);
,1s 后再画。当然视觉效果就好像是,机器处理了 1 s 才放出来,其实不是,机器不到 10 毫秒就基本完成了.......
/* 在画布画图 */
// ctx.drawImage(domImg, 0, 0, domImg_w, domImg_h);
ctx.drawImage(图的< img >, 起画点左坐标, 起画点上坐标, 落笔点右坐标, 落笔点下坐标);
这些照着手册写就行。关键是下面 3 点。
- 把图导出来,导出 base64 格式和独立图片文件。
- 图片黑白化(黑白图片也能为压缩助力)
- 如何计算图片的体积?
把图导出来,导出 base64 格式和独立图片文件。
第一点,canvas 转 base64 好说。现成的 API
out_base64 = c.toDataURL('image/jpeg', imgQuality);
这一句就行了,c 是那个 < canvas >,后面的第二个属性是质量,也就是导出 JPEG 的质量 0 -- 1 之间。压缩质量。比如 0.5。
至于下载独立文件,从网上复制粘贴了个 base64ToFile() 函数就好了。
图片黑白化
第二句图片黑白化。这个可让我真的见识到 JavaScript 是多么快的了。我注释 /* 黑白化 */
下面的句子,把像素点从 < canvas > 一个个取出来,一个个加减乘除分析,就那个 for 循环。诸位可知,随便处理一张图片,这个句子在谈笑间能跑多少次吗?我还专门写了个 console.log ,我的头像,就跑了 20 多万次......
这,要是让我笔算,就我这计算力,一年都算不完。
原理也很简单,就是每个像素点都有 R G B 三个值,只要让 R G B 三个值相等,且等于它们三者的平均数就行。这就是黑白原理了。
如何计算图片体积
第三句,如何计算图片体积?其实已经能拿到图片的 base64 源码了,那离计算其体积就不远了。
根据 base64 的编码原理,六位二进制 101010 可以代表一个字母,但文本格式的 base 64 则需要 10101010 八位二进制才能表示。体积会增长 $\frac{4}{3}$
也就是说,6 kb 的内容,转成 base 64 会变成 8 kb,那直接把 base64 的长度 乘上 0.75 就是文件体积了。
代码如下。
/* 处理后的大小 */
r_imgSize = parseInt(out_base64.length / 1024 * 0.75) + "kb";
结语
至此,程序就完成了。
以后,写文章上传图片,就能上传很小的图片了,太爽了。
不过,以后,也可以再加个 自定义文件名 的功能。这样也便于整理。或者做成 emlog 插件.....
(2023-03-21 补充,插件已经完成 https://www.emlog.net/plugin/detail/557)
因为压缩完的图片,还得再进行下载才行,直接复制会失真...... 目前还没找到把独立文件放到剪切板里的办法,估计这样做也有安全问题。能下载就很不戳了。
补充
2023-03-21 经过 简爱 的提醒,尝试了把 domImg 加上 onload 方法,发现果然奏效,在 < img > 加载完会自动执行 onload 里的函数。
修改后的函数如下所示。
function drawimg(base64data){
creatDomImg(base64data);
// setTimeout(function(){canvdraw()}, 1000); // 1. 注释了原来的延迟执行
}
function creatDomImg(base64data){
s_imgSize = parseInt(base64data.length / 1024 * 0.75) + "kb";
domImg = document.createElement("img");
domImg.src = base64data;
domImg.onload = function() { // 2. 添加了 onload 方法
canvdraw();
}
}
let newblob = new Blob([png], {type:'image/png'});
document.querySelector('.pngsize').innerHTML = "("+ newblob.size +"byte)";
我惊讶的发现那个叫张鑫旭的大佬,去年九月也搞了个这种网页小应用。目前和我这个插件实现的功能一模一样。
https://www.zhangxinxu.com/wordpress/2023/09/js-jpg-png-compress-tinyimg-mini/
当然,我这篇文章是写于前年的,比张大佬早一年。
我的apng頁面則可以通過降低.png的色值索引來降低圖片體積。
.jpg如果壓縮率降到一定程度,就會出現「麻花」,而.png降色值同樣可以很小,不會出現麻花。
不過我佩服的是對.jpg的處理也能做成黑白效果。
.png取色只有3的時候,就會只剩兩種顏色,但不一定是黑白二色。
卧槽,png 的算法是颜色取色!那个值是指保留的颜色,原来是这样!这样的话,可以更大程度上以更小的体积来尽可能保留原图片的轮廓,太棒了!我也要把这个给加上!我研究一下它那取色是怎么自定义的,或许可以先转成 JPG 黑白,然后再 png 取色。
https://ejsoon.win/howtoapng/
https://ejsoon.win/%e3%80%90%e7%9b%b4%e6%92%ad%e3%80%91%e6%88%91%e4%b9%9f%e8%a6%81%e5%81%9aapng%e7%94%9f%e6%88%90%e9%a0%81%e9%9d%a2/2/
地址在此:https://ejsoon.win/apng
domImg.onload = function() {
canvdraw();
}
已补充到文章底部,感谢 o( ̄▽ ̄)d