«

如何用 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 点。

  1. 把图导出来,导出 base64 格式和独立图片文件。
  2. 图片黑白化(黑白图片也能为压缩助力)
  3. 如何计算图片的体积?

把图导出来,导出 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();
    }
  }

标签: 原创 JS 前端

评论:
avatar
ejsoon 2024-05-12 19:46
當轉成blob時,它就自帶size屬性,所以不需要我們再去計算了。


                let newblob = new Blob([png], {type:'image/png'});
                document.querySelector('.pngsize').innerHTML = "("+ newblob.size +"byte)";
commentator
独元殇 2024-05-19 17:46
@ejsoon:前几天,我用这个 upng.js 给我的插件里引入了下, https://www.emlog.net/plugin/detail/557   ,挺实用的,png 是一个很好的替补。

我惊讶的发现那个叫张鑫旭的大佬,去年九月也搞了个这种网页小应用。目前和我这个插件实现的功能一模一样。

https://www.zhangxinxu.com/wordpress/2023/09/js-jpg-png-compress-tinyimg-mini/

当然,我这篇文章是写于前年的,比张大佬早一年。
commentator
ejsoon 2024-05-19 21:41
@独元殇:我也是在張鑫旭那裏知道upng.js的。
commentator
ejsoon 2024-05-19 22:44
@独元殇:張鑫旭的那個我早就實現了,同時我做的還跟apng集成到了一起,也就是把「尺寸調整、png色值調整、jpg壓縮、apng動圖生成」都做進了同一個頁面。

近期我還打算添加.zip功能,把多張圖片打包進一個.zip檔下載。
commentator
ejsoon 2024-05-20 07:40
@ejsoon:同時我那個還有「裁剪圖片」的功能,張鑫旭連裁剪圖片都沒有嗎?張鑫旭最近做的東西有點水啊!
commentator
独元殇 2024-05-19 17:51
@ejsoon:不过,咱这都是跟着 API 拼凑的,网上还是有更专业的东西。 https://squoosh.app/  谷歌的团队搞的,这个才是真正意义上的高精尖信达雅的专业级技术级工具。
commentator
ejsoon 2024-05-19 21:39
@独元殇:https://squoosh.app也是網上很多開源庫的集合。並且我覺的不好用,還不如我做的好用,一直在轉圈圈,不知道是啥問題。
avatar
ejsoon 2024-05-10 14:26
這個工具只能做成.jpg,而不能處理.png。

我的apng頁面則可以通過降低.png的色值索引來降低圖片體積。

.jpg如果壓縮率降到一定程度,就會出現「麻花」,而.png降色值同樣可以很小,不會出現麻花。

不過我佩服的是對.jpg的處理也能做成黑白效果。

.png取色只有3的時候,就會只剩兩種顏色,但不一定是黑白二色。
commentator
独元殇 2024-05-11 09:29
@ejsoon:无法导出 png,不过可以处理 png。

卧槽,png 的算法是颜色取色!那个值是指保留的颜色,原来是这样!这样的话,可以更大程度上以更小的体积来尽可能保留原图片的轮廓,太棒了!我也要把这个给加上!我研究一下它那取色是怎么自定义的,或许可以先转成 JPG 黑白,然后再 png 取色。
commentator
ejsoon 2024-05-13 04:58
@独元殇:降低色值用的是upng.js。

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/
commentator
独元殇 2024-05-11 09:36
@ejsoon:哈哈,有意思,当我把黑白照片扔进你那个程序后,发现怎么调,最终大小都不动了,不过依然比原图小的多,黑白照片在相同处理后的体积,png 的质量显然比 JPG 好很多
avatar
ejsoon 2023-08-01 21:48
我平時也自製一些壓縮圖片、文本處理的小工具,我做的圖片壓縮工具,如果多張可以做成apng動畫,如果是單張那就是精簡圖片,可調尺寸、裁剪、降低色數或壓縮率,可選導出png或jpg。

地址在此:https://ejsoon.win/apng
commentator
独元殇 2023-08-01 22:25
@ejsoon:有点意思,还可以做成 apng 动画。不过很奇怪, apng 至今依然那么小众,可能是体积大吧。
commentator
ejsoon 2023-11-14 18:15
@独元殇:基本上所有的設備都支持apng。它的體積,在我的頁面上是可以調節的。
avatar
简爱 2023-03-21 14:40
<img> 图片地址虽然不是远程地址,但也不是立即就能加载完成的. canvdraw 在 <img> load 事件中调用就行了, 不用延时
commentator
独元殇 2023-03-21 15:15
@简爱:原来如此。确实有效,操作顺滑多了。

    domImg.onload = function() {  
      canvdraw();
    }

已补充到文章底部,感谢 o( ̄▽ ̄)d
avatar
灰常记忆 2022-12-27 17:47
这个好厉害    直接放html就可以?
commentator
独元殇 2022-12-30 00:11
@灰常记忆:算是吧,这个 HTML ,就是一个完整的这个程序。
avatar
2broear 2022-12-14 14:21
可以做成一个插件
avatar
Lvtu 2022-12-14 10:25
优秀!赞赞赞。。。
avatar
沉舟侧畔 2022-12-14 09:57
由于大部分人使用手机拍照,所以手机端有个压缩图片的快捷方式很重要
commentator
独元殇 2022-12-15 22:16
@沉舟侧畔:我用的安卓上有个 叫 camera 4k pro 的 APP,可以极度自由的设置拍摄的尺度,甚至能拍出来 1kb 的照片和 20x20 尺寸的视频。iOS 不清楚。