最近项目中对图片的要求比较高,经常会进行图片压缩和修改分辨率的操作,久而久之就觉得自己写一个吧,于是花了一天的功夫完成了这个脱机版图片压缩工具,无需服务器,本地即可运行。
参考了张鑫旭大佬的两篇文章,链接放在文章的最下方,感兴趣的可以深入了解下。
项目Github地址
首先看看图片上传的效果:
再看看在线获取图片的效果:
CSS
部分不是此次讨论的重点,可以暂时不用考虑,为了美观,这里参考了
antd
的部分组件样式。
原理其实很简单,就是
canvas
的应用。用户上传图片之后我们可以拿到图片的
base64
内容,之后使用
canvas
的
drawImage
方法画出图片,再使用
drawImage
方法时,可以定义图片的宽高和图片质量,不过需要注意的时这里的图片质量选项之后在导出图片为
jpg
时才能有用,导出格式为
png
时是没有任何作用的。
图片大小的计算其实是通过图片的
base64
内容计算出来的。这里就要涉及一点
base64
原理了,拿出小本本记一下:
base64
的出现是因为要兼容除了英文以外的其他语言,因为中文或者日文无法在服务器或者网管上进行有效的处理,经常会出现乱码,这时
base64
就出现了,转化成了一串编码就可以随意传输了,接收到之后再翻译一下就好。
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
let baseStr=base64Url.substring(base64Url.indexOf('base64,')+'base64,'.length)
// 去掉”=“
baseStr = baseStr.replace(/=/gi, '')
// 进行计算
const strLen=baseStr.length
return strLen-(strLen/8)*2
复制代码
首先去掉头部的类型标志,
png
,
jpeg
之类的标识。接下来用正则去掉等号。最后减去填充的0。每8字符串就有两个0,所以用整体长度除以8,再乘以2,可以得到所有0的个数。用总体的长度减去0的个数,即可得到生下的字节数,也就是真正的字节数,再除以1024,得到最终的大小,单位为
KB
。
需要注意的是
MAC
和
windows
的文件体积计算方式不同,
MAC
是1000进制的,而
windows
是1024进制的,所有会有一些区别,这个区别从
MAC
硬盘的容量基本都接近足量也可以看出来。
明显可以看出来,整个页面由两部分构成,左侧是图片压缩信息修改的部分,右侧是使用说明和预览部分。
首先左侧是以一个
wrapper
,用来包裹左侧所有内容。里面分为若干个小部分,首先是最上方的图片自定义宽高部分,代码如下所示:
<div class="wrapper">
<div class="size-options">
<p class="sub-title">图片自定义宽高</p>
<li class="m-b-10">
<span>宽度:</span>
<input class="input-text" type="text" id="custom-width" placeholder="默认值为图片原始尺寸" onChange="updateImage()">
<span>高度:</span>
<input class="input-text" type="text" id="custom-height" placeholder="默认值为图片原始尺寸" onChange="updateImage()">
</div>
</div>
复制代码
很简单的
HTML
,两个
input
而已。在
size-options
下面又增加了到处图片尺寸的配置。代码如下:
<div class="wrapper">
<div class="clarity-options">
<p class="sub-title">导出图片尺寸</p>
<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 class="file-type-option">
<span>图形质量:</span>
<input type="range" name="points" min="1" max="100" id="clarity" value="80" onChange="updateImage()" />
</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>
<span>在线图片:</span>
<input class="input-text" type="url" placeholder="在线图片地址" onChange="getOnlineImage(value)">
</div>
</div>
复制代码
有一个隐藏的
file
控件,因为其样式十分不好调整,干脆就隐藏掉,用下面的
button
来控制其
click
事件。在线图片地址用的也是
HTML5
的
url
新空间,但因为不在一个
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>
复制代码
这就更没什么可说的,简单
<p>
标签,用来展示图片信息。至此,左侧
wrapper
的内容就完结了。下面是右侧
instruction
的内容。
instruction
和
wrapper
是并列关系,也就是
JQuery
中的
siblings
。
instruction
中的内容比较少,也就是使用说明和图片的预览。代码如下所示:
<div class="instruction">
<h4>脱机版图片压缩:使用说明</h4>
<p class="tips">
1.选择图片质量,<strong>注意:只有JPG格式可以调整图片质量,PNG格式无法调整</strong>
<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、5放在了左侧,右侧因为只有使用说明,空间比较大,用来实现4和6是一个比较好的选择。
公共变量的确认与更新
公共变量主要是用户输入的配置信息、图片的基础信息和基础
HTML
部件。
用户输入的信息有:自定义宽度、自定义高度、压缩程度、压缩类型。
图片基础信息有:原始宽度、原始高度。
基础
HTML
部件有:
img
、
canvas
、下载链接。
用户输入信息和用户基础信息很好理解,基础的
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
事件上绑定该方法即可:
<input
name
=
"fileType"
type=
"radio"
value=
"jpeg"
checked
on
Change=
"clarityWeightChange(value)"
>
<label
class
=
"radio-label"
>JPG</label>
<input
name
=
"fileType"
type=
"radio"
value=
"png"
on
Change=
"clarityWeightChange(value)"
>
<label
class
=
"radio-label"
>PNG</label>
复制代码
图片基础信息的获取,这一步相对来说比较复杂,以为要一层层触发,首先需要定义
img
和
reader
两个部件,上面说过
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
事件,所以需要新建一个触发点击的方法,之后再上传按钮上绑定该方法,即可完美触发,代码如下:
<button class="btn m-b-10" onClick="showFileUpload()">上传图片</button>
const showFileUpload = () => {
document.getElementById('file').click();
复制代码
至此,举例项目前期的准备工作都已经完成了,下面开发项目的主体功能部分。
图片的压缩与预览
在压缩图片方法的一开始,应该先获取的用户输入的自定义尺寸信息。使用
document.getElementById()
方法即可。
之后进行目标尺寸的计算。此处的逻辑是如果用户只输入款宽高中的一个是,自动很足图片原始比例等比计算出另一半的长度,也就是说会保持比例不变进行缩放。
那么这是涉及到4种情况:
用户把自定义宽高都填了。这种情况直接将
input
值直接赋值给
customWidth
和
customHeight
变量即可。
用户只填写了自定义宽度,高度没填。这种情况用原始尺寸计算出长宽比,之后乘上自定义宽度,即可得出对应的高度。
用户只填写了自定义高度,宽度没填。这种情况同上,计算出对应宽度即可
用户啥都没填,这就简单了,直接将原始宽高赋值给自定义宽高即可,就也是将
originWidth
赋值给
targetWidth
,将
originHeight
赋值给
targetHeight
。
解决完这四种情况后我们即可拿到最终的目标宽高,此处用到的宽高共有三种,为了防止混淆,下面一一解释:
originWidth
/
originHeight
:用来存储图片的原生宽高,在
img.onload
中进行更新,每次上传图片都会触发更新。
customWidth
/
customHeight
:用来存储用户输入的宽高,此变量只在压缩图片方法中才会用到,在其内部直接使用
document.getElementById()
获取即可。
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
是由
canvas
的
getContext('2d')
得到的一块画布,首先使用
context.clearRect(0, 0, targetWidth, targetHeight)
清空画布,上面
0
参数的作用是定位画布的起点,两个
0
的意思就是从画布的左上角开始,有点类似于绝对定位中的位置,之后的
targetWidth
/
targetHeight
参数是确定应该清空画布的长宽,从而确定整张画布的大小。
下面使用
context.drawImage(img, 0, 0, targetWidth, targetHeight)
方法来进行图片的绘制,第一个参数就是上文提到的
img
部件,里面存储着图片的
base64
编码,第二个剩下的参数和
context.getContext()
方法中的参数一样,在此不多做赘述。
下面就是最后一步了,使用
canvas
的
toDataURL()
方法得到压缩后图片的
base64
编码。改方法接收两个参数,第一个是压缩后的图片类型,比方说
image/png
、
image/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)
文件体积的计算在上面的原理部分已经将的很详细了,这里只需反向思考即可得出文件的大小,不过这里没有根据windows
和MAC
系统的不同来修改进制,统一是1024
的进制,有兴趣的同学可以改进下。代码如下:
const getFileSize = (base64Url) => {
// 去掉无用头部信息(data:image/png
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
方法就是压缩图片的主要方法,在完成这个方法后,需要将其绑定在所有相关图片配置的选项上,如此一修改配置,用户即可看见预览图的变化,举个例子:
<li class="m-b-10">
<span>宽度:</span>
<input class="input-text" type="text" id="custom-width" placeholder="默认值为图片原始尺寸" onChange="updateImage()">
<span>高度:</span>
<input class="input-text" type="text" id="custom-height" placeholder="默认值为图片原始尺寸" onChange="updateImage()">
复制代码
剩下的还有导出图片类型和图形质量选项,依次绑定即可。
剩下的部分就是在线图片的获取了,这里的方法十分简单,将图片地址赋值给
img
部件即可,代码如下:
// 获取在线图片地址
const getOnlineImage = (value) => {
img.src = value
复制代码
修改
img
的
src
属性会自动触发
img.onload
方法,之后也会顺其自然的根据当前配置来进行图片的压缩。
需要注意的一点就是有的图片可能会跨域,此时需要给
img
部件增加一个参数,如下:
img.setAttribute("crossOrigin",'Anonymous')
复制代码
如此即可简单的解决跨域问题,当然了,有些图片这样可能还是获取不了,这是就需要更加强大的功能来实现了,笔者这里有点懒,就先这样凑合了,以后有时间再改吧。
还有一点就是
<input type='file'>
元素会出现相同文件上传不变化的问题,这就有点尴尬了,可以将
eleFile
部件的
value
属性置空,让它以为自己目前没有图片,这样再次上传相同文件也就没有问题了。代码放在了
updateImgae
方法压缩完成功能的后面:
const updateImage = () => {
其他内容...
// 存储下载文件名
eleLink.download = `${targetWidth}_${targetHeight}`
// 清除当前文件的路径值,避免不能上传同一张图片的问题
eleFile.value = ''
其他内容...
复制代码
总结
好啦,到了这里整个项目也就完成了,难度始终,好好去研究的话问题不大。需求的确认比较关键,笔者写的时候使用的事“渐进增强”的方法,逐步增加更多的功能,到了后面会发现之前的代码很多都没有用,这就比较浪费时间了,所以希望各位读者可以吸取笔者的教训,开始开发之前一定要想好需求,否则真的会浪费很多时间的。
看了这么久,辛苦了,诸位!
张鑫旭大佬的两篇文章:
www.zhangxinxu.com/wordpress/2…
www.zhangxinxu.com/wordpress/2…