JS脱机版:图片压缩工具

前言

最近项目中对图片的要求比较高,常常会进行图片压缩和修改分辨率的操做,长此以往就以为本身写一个吧,因而花了一天的功夫完成了这个脱机版图片压缩工具,无需服务器,本地便可运行。html

参考了张鑫旭大佬的两篇文章,连接放在文章的最下方,感兴趣的能够深刻了解下。html5

项目Github地址git

效果展现

首先看看图片上传的效果:github

再看看在线获取图片的效果:json

CSS部分不是这次讨论的重点,能够暂时不用考虑,为了美观,这里参考了antd的部分组件样式。canvas

原理介绍

原理其实很简单,就是canvas的应用。用户上传图片以后咱们能够拿到图片的base64内容,以后使用canvasdrawImage方法画出图片,再使用drawImage方法时,能够定义图片的宽高和图片质量,不过须要注意的时这里的图片质量选项以后在导出图片为jpg时才能有用,导出格式为png时是没有任何做用的。windows

图片大小的计算实际上是经过图片的base64内容计算出来的。这里就要涉及一点base64原理了,拿出小本本记一下:跨域

base64的出现是由于要兼容除了英文之外的其余语言,由于中文或者日文没法在服务器或者网管上进行有效的处理,常常会出现乱码,这时base64就出现了,转化成了一串编码就能够随意传输了,接收到以后再翻译一下就好。bash

base64的原理比较简单,base64有一个本身的表,里面每一个字节都有着本身的代号。服务器

首先,将须要待转换的字符串分红三个一组,每一个字节的大小时8bit,那么三个字节就有24个二进制位。

而后,再将上面的24个二级制位分红4组,每组6个。

接下来,在每组前面增长两个0,因而每组变成了8个二级制为,4组总共32个二进制位,总共4个字节。

最后,根据以前说过的base64转换表,将这些二级制位翻译一下,就获得了最终的base64字符串。

那么这里有两个问题:首先,由于在每组以前都增长了两个0,因此base64编码以后的文本会比原生文本大三分之一左右。其次就是为何要使用3个字节一组呢?那是由于6和8的最小公约数时24,3个8和4个6正好都是24。

还有一个特殊状况就是万一有的位数不足怎么办呢?分的时候可能字节数不足三个。若是字节数是2个,能够拿到16个二进制位,6个一组以后,最后一组差两个,用0补齐正好3组,但是第四组呢?这时候就须要用=来伪装这里是一个组了,强行凑够4组。如果只有一个字节数,那么12除以6等于2,还差两个组才能到四,因此须要两个=来凑个四个组。为了4组也是蛮拼的。因此说base64编码中可能会出现一到两个=

知道这些以后咱们就能够反向计算文件体积了,代码以下所示:

const getFileSize = (base64Url) => {
  //  去掉无用头部信息(data:image/png;base64,)
  let baseStr=base64Url.substring(base64Url.indexOf('base64,')+'base64,'.length);
  //  去掉”=“
  baseStr = baseStr.replace(/=/gi, '');
  // 进行计算
  const strLen=baseStr.length;
  return strLen-(strLen/8)*2
}
复制代码

首先去掉头部的类型标志,pngjpeg之类的标识。接下来用正则去掉等号。最后减去填充的0。每8字符串就有两个0,因此用总体长度除以8,再乘以2,能够获得全部0的个数。用整体的长度减去0的个数,便可获得生下的字节数,也就是真正的字节数,再除以1024,获得最终的大小,单位为KB

须要注意的是MACwindows的文件体积计算方式不一样,MAC是1000进制的,而windows是1024进制的,全部会有一些区别,这个区别从MAC硬盘的容量基本都接近足量也能够看出来。

页面布局

明显能够看出来,整个页面由两部分构成,左侧是图片压缩信息修改的部分,右侧是使用说明和预览部分。

首先左侧是以一个wrapper,用来包裹左侧全部内容。里面分为若干个小部分,首先是最上方的图片自定义宽高部分,代码以下所示:

<div class="wrapper">
  <div class="size-options">
    <p class="sub-title">图片自定义宽高</p>
    <ul>
      <li class="m-b-10">
        <span>宽度:</span>
        <input class="input-text" type="text" id="custom-width" placeholder="默认值为图片原始尺寸" onChange="updateImage()">
      </li>
      <li>
        <span>高度:</span>
        <input class="input-text" type="text" id="custom-height" placeholder="默认值为图片原始尺寸" onChange="updateImage()">
      </li>
    </ul>
  </div>
</div>
复制代码

很简单的HTML,两个input而已。在size-options下面又增长了处处图片尺寸的配置。代码以下:

<div class="wrapper">
  // 其余内容
  <div class="clarity-options">
    <p class="sub-title">导出图片尺寸</p>
    <ul>
      <li>
        <input name="fileType" type="radio" value="jpeg" checked onChange="clarityWeightChange(value)">
        <label class="radio-label">JPG</label>
        <input name="fileType" type="radio" value="png" onChange="clarityWeightChange(value)">
        <label class="radio-label">PNG</label>
      </li>
      <li class="file-type-option">
        <span>图形质量:</span>
        <input type="range" name="points" min="1" max="100" id="clarity" value="80" onChange="updateImage()" />
      </li>
    </ul>
  </div>
</div>
复制代码

布局和size-options十分相似,多了一个HTML5中的range标签,会根据fileType的类型来展现和隐藏。再下面就是上传图片和在线图片地址的选项。代码以下:

<div class="wrapper">
  // 其余内容
  <input class="hidden" type="file" id="file">
  <button class="btn m-b-10" onClick="showFileUpload()">上传图片</button>
  <div>
    <span>在线图片:</span>
    <input class="input-text" type="url" placeholder="在线图片地址" onChange="getOnlineImage(value)">
  </div>
</div>
复制代码

有一个隐藏的file控件,由于其样式十分很差调整,干脆就隐藏掉,用下面的button来控制其click事件。在线图片地址用的也是HTML5url新空间,但由于不在一个form中,因此好像用处不大,用text也没什么影响。最下面是图片信息的展现。代码以下:

<div class="wrapper">
  // 其余内容
  <div class="display">
    <div class="img-details hidden">
      <p>图片信息:</p>
      <p class="indent-2" id="img-size"></p>
      <p class="indent-2" id="img-origin-weight"></p>
      <p class="indent-2" id="img-now-weight"></p>
      <canvas id="canvas"></canvas>
    </div>
  </div>
</div>
复制代码

这就更没什么可说的,简单<p>标签,用来展现图片信息。至此,左侧wrapper的内容就完结了。下面是右侧instruction的内容。

instructionwrapper是并列关系,也就是JQuery中的siblingsinstruction中的内容比较少,也就是使用说明和图片的预览。代码以下所示:

<div class="instruction">
  <h4>脱机版图片压缩:使用说明</h4>
  <p class="tips">
    1.选择图片质量,<strong>注意:只有JPG格式能够调整图片质量,PNG格式没法调整</strong>
  </p>
  <div class="img-preview hidden">
    <button class="btn" onClick="downloadImage()">下载图片</button>
    <p>预览:</p>
    <img id="img-display" src="" alt="">
  </div>
</div>
复制代码

ok,到这里HTML部分的内容就完成了,上面的代码中有不少function,暂时能够先无论了,下面会一一进行讲解。

具体实现

下面咱们将从功能分析到具体的的实现来一步步完成这个脱机的图片压缩工具。

功能分析

功能嘛,其实主要就是上传而且压缩图片,但压缩的时候须要按照用户的需求来具体的压缩,尺寸和清晰度都要考虑到。

其次就是图片压缩完成后怎么给用户一个反馈,让用户意识到图片已经压缩完成了,这里分红两个部分,一个是直观的图片展现,一个是图片转换先后信息变化的展现。如此,无论是专业的用户仍是普通用户均可以明显的看到压缩的效果。

最后,也是最重要的——下载功能。没有下载功能整个项目就等于不存在了。

综上所述,咱们须要完成如下几点功能:

  1. 图片上传
  2. 图片尺寸自定义
  3. 图片清晰度自定义
  4. 图片预览
  5. 图片信息展现
  6. 图片下载

那么根据上面提到的页面布局,为了全面、合理的展现页面的所有内容,这里将一、二、三、5放在了左侧,右侧由于只有使用说明,空间比较大,用来实现4和6是一个比较好的选择。

公共变量的确认与更新

公共变量主要是用户输入的配置信息、图片的基础信息和基础HTML部件。

用户输入的信息有:自定义宽度、自定义高度、压缩程度、压缩类型。

图片基础信息有:原始宽度、原始高度。

基础HTML部件有:imgcanvas、下载连接。

用户输入信息和用户基础信息很好理解,基础的HTML部件其实就是此次项目中用来实际操做图片的东西,img部件用来存储用户上传的图片相关信息,同时如果用户上传图片地址的话,也能够存储在线图片的内容。

canvas部件不用多说,是用来画图的,是压缩图片的关键所在。

最后的下载连接为的是实现图片下载功能,将图片信息存在一个<a>标签中,以后模拟触发click事件,来实现图片的下载。

在弄清楚全部的部件以后便可开始开发初期的准备工做了,将公用变量和HTML绑定起来,HTML中内容的变化会触发公共变量的变化,也就是一个单向绑定。

首先想到的应该就是最上方图片自定义尺寸选项,其实这里的宽高能够不存成全局变量,由于只有在压缩图片是才会用到这两个参数,其余地方彻底不会用到,因此直接在方法内部获取就行了,省了存成全局变量,混淆视听,同理还有压缩程度参数。

那么下面就是导出图片选项了,首先是导出图片的格式,这里存成imgType,初始值是jpeg。而后新建方法来同步信息,而且在选项为PNG时,隐藏图形质量选项,代码以下:

const clarityWeightChange = (value) => {
  imgType = value;
  document.getElementsByClassName("file-type-option")[0].classList.toggle('hidden');
  updateImage()
}
复制代码

首先赋值给imgType变量,由于只有两个选项,因此直接toggle一下hidden属性便可,那么图形选项框就会在隐藏和展现之间来回切换,无需判断当前的值是PNG仍是JPG了。再在DOM元素的onChange事件上绑定该方法便可:

<li>
  <input name="fileType" type="radio" value="jpeg" checked onChange="clarityWeightChange(value)">
  <label class="radio-label">JPG</label>
  <input name="fileType" type="radio" value="png" onChange="clarityWeightChange(value)">
  <label class="radio-label">PNG</label>
</li>
复制代码

图片基础信息的获取,这一步相对来讲比较复杂,觉得要一层层触发,首先须要定义imgreader两个部件,上面说过img是用来存储文件信息的,那么reader是用来读取图片信息的,在读图片信息以后img才能获取到图片的宽高信息。那么问题来了reader是怎样获取到图片信息的呢?这时候就须要eleFile了,eleFile是用来获取到用户上传文件的input输入框的信息。

因此就是首先经过eleFile获取上传的图片信息,接下来触发reader的方法,获取到图片的base64编码,由于在eleFile中是没法获取到的。在reader获取到base64编码后,再触发img方法,此时便可获取到上传图片的原始尺寸。

const reader = new FileReader();
const img = new Image();
const eleFile = document.getElementById("file");

// 文件base64化,获取图片原始尺寸
img.onload = (image) => {
  originWidth = +image.path[0].width;
  originHeight = +image.path[0].height;
};
// 赋值图片信息给img
reader.onload = function(e) {
  img.src = e.target.result;
};
// 读取原始文件信息
eleFile.addEventListener('change', (event) => {
  reader.readAsDataURL(event.target.files[0]);
});
复制代码

经过这三步便可获取图片的原始尺寸。

那么最后剩下的就是canvas和下载连接了,这没啥好说的,代码以下所示:

const canvas = document.getElementById('canvas')
const context = canvas.getContext('2d');
const eleLink = document.createElement('a');
eleLink.style.display = 'none';
复制代码

由于下载连接的展现毫无心义,因此直接隐藏就好,下载这里可能会有点问题,不过不影响使用,后面会有详细解释。

准备工做还有一步就是上传文件input的触发,为了样式方便,这里将该input隐藏,要想触发只能经过click事件,因此须要新建一个触发点击的方法,以后再上传按钮上绑定该方法,便可完美触发,代码以下:

// HTML
<button class="btn m-b-10" onClick="showFileUpload()">上传图片</button>

// JS
const showFileUpload = () => {
  document.getElementById('file').click();
}
复制代码

至此,举例项目前期的准备工做都已经完成了,下面开发项目的主体功能部分。

图片的压缩与预览

在压缩图片方法的一开始,应该先获取的用户输入的自定义尺寸信息。使用document.getElementById()方法便可。

以后进行目标尺寸的计算。此处的逻辑是若是用户只输入款宽高中的一个是,自动很足图片原始比例等比计算出另外一半的长度,也就是说会保持比例不变进行缩放。

那么这是涉及到4种状况:

  1. 用户把自定义宽高都填了。这种状况直接将input值直接赋值给customWidthcustomHeight变量便可。

  2. 用户只填写了自定义宽度,高度没填。这种状况用原始尺寸计算出长宽比,以后乘上自定义宽度,便可得出对应的高度。

  3. 用户只填写了自定义高度,宽度没填。这种状况同上,计算出对应宽度便可

  4. 用户啥都没填,这就简单了,直接将原始宽高赋值给自定义宽高便可,就也是将originWidth赋值给targetWidth,将originHeight赋值给targetHeight

解决完这四种状况后咱们便可拿到最终的目标宽高,此处用到的宽高共有三种,为了防止混淆,下面一一解释:

  1. originWidth/originHeight:用来存储图片的原生宽高,在img.onload中进行更新,每次上传图片都会触发更新。

  2. customWidth/customHeight:用来存储用户输入的宽高,此变量只在压缩图片方法中才会用到,在其内部直接使用document.getElementById()获取便可。

  3. targetWidth/targetHeight:用来肯定压缩是的尺寸,由于用户有时输入的信息不全,可能会出现上面的4种状况,因此须要计算得出最终长宽。

const customWidth = +document.getElementById('custom-width').value;
const customHeight = +document.getElementById('custom-height').value;
//  判断宽高填写的四种状况
if (customWidth && customHeight) {
  targetWidth = customWidth;
  targetHeight = customHeight;
} else if (customWidth && !customHeight) {
  targetWidth = customWidth;
  targetHeight = Math.round(targetWidth * (originHeight / originWidth));
} else if (!customWidth && customHeight) {
  targetHeight = customHeight;
  targetWidth = Math.round(targetHeight * (originHeight / originWidth));
} else {
  targetWidth = originWidth;
  targetHeight = originHeight
}
复制代码

Ok,如今图片的宽高已经彻底弄清楚了,先更新下画布大小,将其改成targetWidth/targetHeight,不然图片没法总体压缩。

下面使用context进行图片的绘制,context是由canvasgetContext('2d')获得的一块画布,首先使用context.clearRect(0, 0, targetWidth, targetHeight)清空画布,上面0参数的做用是定位画布的起点,两个0的意思就是从画布的左上角开始,有点相似于绝对定位中的位置,以后的targetWidth/targetHeight参数是肯定应该清空画布的长宽,从而肯定整张画布的大小。

下面使用context.drawImage(img, 0, 0, targetWidth, targetHeight)方法来进行图片的绘制,第一个参数就是上文提到的img部件,里面存储着图片的base64编码,第二个剩下的参数和context.getContext()方法中的参数同样,在此很少作赘述。

下面就是最后一步了,使用canvastoDataURL()方法获得压缩后图片的base64编码。改方法接收两个参数,第一个是压缩后的图片类型,比方说image/pngimage/jpeg等,第二个参数就是图片的质量,是一个从0到1的小数,数字越大越清晰,此处能够从上面的<input type='range'>中拿到改变量。

// canvas对图片进行缩放
canvas.width = targetWidth;
canvas.height = targetHeight;
// 清除画布
context.clearRect(0, 0, targetWidth, targetHeight);
// 图片压缩
context.drawImage(img, 0, 0, targetWidth, targetHeight);
// 存储图片base64连接
let imgCompressed = '';
if (imgType === 'png') {
  imgCompressed = canvas.toDataURL('image/png');
} else {
  imgCompressed = canvas.toDataURL('image/jpeg', +document.getElementById('clarity').value / 100);
}
// 图片预览
document.getElementById('img-display').setAttribute('src', imgCompressed);
// 图片信息展现
imgInfo = `图片尺寸:${targetWidth} * ${targetHeight} (长 * 宽)(单位:像素)`;
document.getElementById('img-size').innerHTML = imgInfo;
document.getElementsByClassName('img-details')[0].classList.remove('hidden');
document.getElementsByClassName('img-preview')[0].classList.remove('hidden');
复制代码

在上面的代码中,完成图片压缩后将图片的base64编码放到了img标签中,用来展现压缩后的文件,同时将预览和下载按钮展现出来,方面用户查阅。

至此,图片压缩部分已经完成了,剩下的就只有最后的图片下载功能了。

图片的下载与体积的计算

图片下载的原理是新建一个<a>标签,以后将压缩后的图片base64编码放到<a>href属性中,以后将<a>添加到页面中,触发其click事件,再将其删掉,便可完成这次下载。

这样作的问题是可能会有的朋友以为很麻烦,由于增长和删除元素的操做有点费劲。其实也还好,感受这样作省去了直接在页面上增长<a>标签,结构上会更整洁些。

代码实现以下所示:

const eleLink = document.createElement('a');
eleLink.style.display = 'none';
//  肯定下载连接
eleLink.href = imgCompressed;
//  肯定下载文件名
eleLink.download = `${targetWidth}_${targetHeight}`;
//  下载文件方法
const downloadImage = () => {
  //  添加元素
  document.body.appendChild(eleLink);
  //  触发点击
  eleLink.click();
  //  而后移除
  document.body.removeChild(eleLink);
}
复制代码

文件体积的计算在上面的原理部分已经将的很详细了,这里只需反向思考便可得出文件的大小,不过这里没有根据windowsMAC系统的不一样来修改进制,统一是1024的进制,有兴趣的同窗能够改进下。代码以下:

const getFileSize = (base64Url) => {
  //  去掉无用头部信息(data:image/png;base64,)
  let baseStr=base64Url.substring(base64Url.indexOf('base64,')+'base64,'.length);
  //  使用正则去掉”=“
  baseStr = baseStr.replace(/=/gi, '');
  //  进行计算
  const strLen=baseStr.length;
  return strLen-(strLen/8)*2
}
复制代码

以后咱们增长如下文件体积的展现:

const updateImage = () => {
  绘制图片内容...
  // 存储图片base64连接
  if (imgType === 'png') {
    eleLink.href = canvas.toDataURL('image/png');
  } else {
    eleLink.href = canvas.toDataURL('image/jpeg', +document.getElementById('clarity').value / 100);
  }
  // 存储下载文件名
  eleLink.download = `${targetWidth}_${targetHeight}`;
  // 图片信息展现
  imgInfo = `图片尺寸:${targetWidth} * ${targetHeight} (长 * 宽)(单位:像素)`;
  document.getElementById('img-size').innerHTML = imgInfo;
  其余内容...
}
复制代码

updateImage方法就是压缩图片的主要方法,在完成这个方法后,须要将其绑定在全部相关图片配置的选项上,如此一修改配置,用户便可看见预览图的变化,举个例子:

<ul>
  <li class="m-b-10">
    <span>宽度:</span>
    <input class="input-text" type="text" id="custom-width" placeholder="默认值为图片原始尺寸" onChange="updateImage()">
  </li>
  <li>
    <span>高度:</span>
    <input class="input-text" type="text" id="custom-height" placeholder="默认值为图片原始尺寸" onChange="updateImage()">
  </li>
</ul>
复制代码

剩下的还有导出图片类型和图形质量选项,依次绑定便可。

其余

剩下的部分就是在线图片的获取了,这里的方法十分简单,将图片地址赋值给img部件便可,代码以下:

//  获取在线图片地址
const getOnlineImage = (value) => {
  img.src = value;
}
复制代码

修改imgsrc属性会自动触发img.onload方法,以后也会顺其天然的根据当前配置来进行图片的压缩。

须要注意的一点就是有的图片可能会跨域,此时须要给img部件增长一个参数,以下:

//  开启图片地址的跨域
img.setAttribute("crossOrigin",'Anonymous')
复制代码

如此便可简单的解决跨域问题,固然了,有些图片这样可能仍是获取不了,这是就须要更增强大的功能来实现了,笔者这里有点懒,就先这样凑合了,之后有时间再改吧。

还有一点就是<input type='file'>元素会出现相同文件上传不变化的问题,这就有点尴尬了,能够将eleFile部件的value属性置空,让它觉得本身目前没有图片,这样再次上传相同文件也就没有问题了。代码放在了updateImgae方法压缩完成功能的后面:

const updateImage = () => {
  其余内容...
  // 存储下载文件名
  eleLink.download = `${targetWidth}_${targetHeight}`;
  // 清除当前文件的路径值,避免不能上传同一张图片的问题
  eleFile.value = '';
  其余内容...
}
复制代码

总结

好啦,到了这里整个项目也就完成了,难度始终,好好去研究的话问题不大。需求的确认比较关键,笔者写的时候使用的事“渐进加强”的方法,逐步增长更多的功能,到了后面会发现以前的代码不少都没有用,这就比较浪费时间了,因此但愿各位读者能够吸收笔者的教训,开始开发以前必定要想好需求,不然真的会浪费不少时间的。

看了这么久,辛苦了,诸位!

PS

张鑫旭大佬的两篇文章:
www.zhangxinxu.com/wordpress/2…
www.zhangxinxu.com/wordpress/2…

相关文章
相关标签/搜索