任何研究都不是凭空产生,至少博主是这样的。
在手机端拍照后图片很大,有的甚至有 10M 多。这个时候再去上传图片,可想而知,速度是很慢的。正因如此,便有了前端图片压缩这个需求。
传统的图片格式有 git、jpg/jpeg、png 等,谷歌后来推出了另一种压缩比更高的格式 webp。
在介绍这几种图片格式前,先说下图片的一些特性。
有损和无损
索引色和直接色
rgb(255, 255, 255)
这种方式表示颜色,所以不难理解这种方式呈现的颜色大约是 2^24 种。点阵图和矢量图
格式 | 特性 | 备注 |
---|---|---|
bmp | 无损、直接色、点阵图 | 体积较大,目前很少适用了 |
git | 无损、索引色、点阵图 | 适用于动图 |
jpg/jpeg | 有损、直接色、点阵图 | 适用于色彩丰富的图像,如风景和人物照片 |
png8 | 无损、索引色、点阵图 | 适用于有透明要求,不需要特别丰富色彩的图像 |
png24 | 无损、直接色、点阵图 | 适用于有透明要求,需要特别丰富色彩的图像。该格式体积要比 jpg 大的多 |
webp | 无损/无损、直接色、点阵图 | 适用于网页,但是目前兼容性稍差 |
如果自己从底层实现图片压缩,那可以涉及到颜色编码处理了。对于,前端工程师来说,难度还是很大的。博主这里提到的图片压缩是借助 Canvas 的 API 来实现图片的压缩处理。
先简要说下思路:
这里给出代码:
<input type="file" id="file">
<a id="downImg" download="compress">downloada>
<script>
const file = document.getElementById('file')
file.addEventListener('change', function (e) {
const imageFile = e.target.files[0]
const imgUrl = URL.createObjectURL(imageFile)
const imgEle = document.createElement('img')
imgEle.src = imgUrl
imgEle.onload = function () {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = imgEle.width
canvas.height = imgEle.height
ctx.drawImage(imgEle, 0, 0)
const data = canvas.toDataURL('image/jpeg', 0.7)
const downImg = document.getElementById('downImg')
downImg.href = data
}
}, false)
script>
看到这里,基本上就实现了图片压缩的基本需求。但如果是这么简单,博主就不会专门写一篇文章来介绍了。
一般来说,我们压缩图片,是希望能够无损压缩的。但是这种无损压缩对于前端来说,比较困难。博主做了一些研究,还没有找到用 JS 实现的方式。
而有损压缩,有两个方向:
这个比较简单就是调整一个参数值。
canvas.toDataURL(type, encoderOptions);
encoderOptions:在指定图片格式为 image/jpeg
或 image/webp
的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。
经过一些测试比较,encoderOptions 设为 0.7 比较合适(这个测试比较粗糙,仅供参考)。既能保证比较大的压缩比,又能较少损失图片的成像质量。
一般来说我们是等比例的缩小原图,canvas 提供了一个 API 可以很方便的帮助我们去缩小图片的大小。
ctx.drawImage(image, dx, dy, dWidth, dHeight)
如果我们要调整图片的大小,只需要改变 dWidth 和 dHeight 的值即可。
如果我们的诉求比较简单,那么我们可以采用简单的设计思路。
经过我们测试发现,图片在 200k 以下,我们的传输速度在 1 秒以内,可以接受。所以,如果原图小于 200k,我们不进行压缩。
如果原图是 png 格式,我们需要衡量下是否进行压缩。因为 canvas 是不支持无损的 png 压缩的。如果我们要进行压缩,那就需要转为 jpeg 或者 webp 格式,同时 png 的透明效果将会消失。
一般来说,我们是希望保存原始图片的长宽的。那么我们可用的方式就是改变图片的质量了,前面已经提到比较适合的一个值是 0.7。通过这个设置,一般图片的压缩率可以达到 60~95%。图片越大,压缩的效果越明显。png 格式转为 jpeg 情况压缩率也非常高。
前面的算法,虽然可以帮助我们较大程度的压缩图片,但是最终的体积还是不确定。有的时候,我们希望图片最终压缩到一个指定的大小。
从前面的内容来看,并没有什么方法可以使图片压缩到指定的大小。不过,这里我们有一个接近指定大小的压缩算法,那就是递归压缩。当我们第一次压缩后,如果得到的图片大小和指定的大小不一样,我们就再次进行压缩。
这里我们需要注意,即使是递归压缩,我们也没办法指定一个确定的大小。因为很难在某一次压缩后,图片的大小正好是我们期望的大小。如果不理解,先仔细想一想。
既然不能给一个确定的值,那我们该怎么给出大小呢?答案就是给一个范围。只要图片压缩后的大小落在这个范围内,那么我们就停止压缩。
这里,博主不建议只给一个边界值,因为这样图片可能压缩的过小,导致图片质量过差而无法查看。 我们可以给一个下界和一个上界,当图片小于下界时,我们重新压缩将其调大。
还有一个情况需要注意,有可能我们给了一个范围。但是最终无论我们怎么调整,图片都无法压缩到指定大小,这个时候就会陷入死循环。所以,我们需要统计压缩次数。当压缩的次数大于某个值时,我们就停止压缩。
目前这个图片压缩算法基本可以投入使用了,但是在实际使用中,博主还发现了一些其他问题。
一些安卓机型,比如红米等,拍出的照片直接预览没有问题。但是上传后,再通过 img 标签或者背景图显示时,就会发现照片发生了旋转,本来竖着的照片横着呈现了。
这个问题比较主流的方法就是通过 exif-js 这个库来获取图片的方向,然后再进行处理。
打个比方:如果我们预览的图片的方向值为 6,那么我们只需要将其顺时针旋转 90 度即可正常查看。
关于旋转的内容可以查看 http://sylvana.net/jpegcrop/exif_orientation.html
如果图片有透明区域,上传后,由于压缩成了 jpeg 格式,所以透明区域消失了,默认呈现黑色。有个比较简单的处理方法,我们在将图片画到 canvas 之前,先将 canvas 的背景设为白色。但是如果原图非透明部分的边缘是白色,那样效果会很糟糕。
ctx.fillStyle = '#fff'
ctx.fillRect(0, 0, canvas.width, canvas.height)
在部分 IOS 机型上,如果图片的分辨率过大,那么将无法转化成 Base64 数据。
参考: