利用 canvas 实现数据压缩

前言

HTTP 支持 GZip 压缩,可节省不少传输资源。但遗憾的是,只有下载才有,上传并不支持。

如果上传也能压缩,那就完美了。特别适合大量文本提交的场合,比如博客园,就是很好的例子。

虽然标准不支持「上传压缩」,但仍可以自己来实现。

Flash

首选方案当然是 Flash,毕竟它提供了压缩 API。除了 zip 格式,还支持 lzma 这种超级压缩。

因为是原生接口,所以性能极高。而且对应的 swf 文件,也非常小。

JavaScript

Flash 逐渐淘汰,但取而代之的 HTML5,却没有提供压缩 API。只能自己用 JS 实现。

这虽然可行,但运行速度就慢多了,而且相应的 JS 也很大。

如果代码有 50kb,而数据压缩后只小 10kb,那就不值了。除非量大,才有意义。

其他

能否不用 JS,而是利用某些接口,间接实现压缩?

事实上,在 HTML5 刚出现时,就注意到了一个功能:canvas 导出图片。可以生成 jpg、png 等格式。

如果在思考的话,相信你也想到了。没错,就是 png —— 它是无损压缩的。

我们把普通数据当成像素点,画到 canvas 上,然后导出成 png,就是一个特殊的压缩包了~


下面开始探索。。。

数据转换

数据转像素,并不麻烦。1 个像素可以容纳 4 个字节:

事实上有现成的方法,可批量将数据填充成像素:

但是,图片的宽高如何设定?

尺寸设定

最简单的,就是用 1px 的高度。比如有 1000 个像素,则填在 1000 x 1 的图片里。

但如果有 10000 像素,就不可行了。因为 canvas 的尺寸,是有限制的。

不同的浏览器,最大尺寸不一样。有 4096 的,也有 32767 的。。。

以最大 4096 为例,如果每次都用这个宽度,显然不合理。

比如有 n = 4100 个像素,我们使用 4096 x 2 的尺寸:

第二行只用到 4 个,剩下的 4092 个都空着了。

但 4100 = 41 * 100。如果用这个尺寸,就不会有浪费。

所以,得对 n 分解因数:

这样就能将 n 个像素,正好填满 w x h 的图片。

但 n 是质数的话,就无解了。这时浪费就不可避免了,只是,怎样才能浪费最少?

于是就变成这样一个问题:

如何用 n + m 个点,拼成一个 w x h 的矩形(0

考虑到 MAX 不大,穷举就可以。

我们遍历 h,计算相应的 w = ceil(n / h), 然后找出最接近 n 的 w * h。

因为 w * h 和 h * w 是一样的,所以只需遍历到 sqrt(n) 就可以。

同样,也无需从 1 开始,从 n / MAX 即可。

这样,我们就能找到最适合的图片尺寸。

当然,连续的空白像素,最终压缩后会很小。这一步其实并不特别重要。

渲染问题

定下尺寸,我们就可以「渲染数据」了。

然而现实中,总有些意想不到的坑。canvas 也不例外:

读取的像素,居然和写入的有偏差!而且不同的浏览器,偏差还不一样。

原来,浏览器为了提高渲染性能,有一个 Premultiplied Alpha 的机制。但是,这会牺牲一些精度!

虽然视觉上并不明显,但用于数据存储,就有问题了。

如何禁用它?一番尝试都没成功。于是,只能从数据上琢磨了。

如果不使用 Alpha 通道,又会怎样?

这样,倒是避开了问题。

看来,只能从数据上着手,跳过 Alpha 通道:

这时,就不受 Premultiplied Alpha 的影响了。

出于简单,也可以 1 像素存 1 字节:

这样,整个图片最多只有 256 色。如果能导出成「索引型 PNG」的话,也是可以尝试的。

数据编码

最后,就是将图像进行导出。

如果 canvas 能直接导出成 blob,那是最好的。因为 blob 可通过 AJAX 上传。

不过,大多浏览器都不支持。只能导出 data uri 格式:

但 base64 会增加长度。所以,还得解回二进制:

这时的 binary,就是最终数据了吗?

如果将 binary 通过 AJAX 提交的话,会发现实际传输字节,比 binary.length 大。

原来 atob 返回的数据,仍是字符串型的。传输时,就涉及字集编码了。

因此还需再转换一次,变成真正的二进制数据:

这时的 buf,才能被 AJAX 原封不动的传输。

最终效果

综上所述,我们简单演示下:Demo

找一个大块的文本测试。例如 qq.com 首页 HTML,有 637,101 字节。

先使用「每像素 1 字节」的编码,各个浏览器生成的 PNG 大小:

Chrome FireFox Safari
体积 289,460 203,276 478,994
比率 45.4% 31.9% 75.2%

其中火狐压缩率最高,减少了 2/3 的体积。

生成的 PNG 看起来是这样的:

不过遗憾的是,所有浏览器生成的图片,都不是「256 色索引」的。


再测试「每像素 3 字节」,看看会不会有改善:

Chrome FireFox Safari
体积 297,239 202,785 384,183
比率 46.7% 31.8% 60.3%

Safari 有了不少的进步,不过 Chrome 却更糟了。

FireFox 有略微的提升,压缩率仍是最高的。

同样遗憾的是,即使整个图片并没有用到 Alpha 通道,但生成的 PNG 仍是 32 位的。

而且,也无法设置压缩等级,使得这种压缩方式,效率并不高。

相比 Flash 压缩,差距就大多了:

deflate 压缩 lzma 压缩
体积 133,660 108,015
比率 21.0% 17.0%

并且 Flash 生成的是通用格式,后端解码时,使用标准库即可。

而 PNG 还得位图解码、像素处理等步骤,很麻烦。

所以,现实中还是优先使用 Flash,本文只是开脑洞而已。

实际用途

不过这种方式,实际还是有用到过。用在一个较大日志上传的场合(并且不能用 Flash)。

因为后端并不分析,仅仅储存而已。所以,可以将日志对应的 PNG 下回本地,在管理员自己电脑上解析。

解压更容易,就是将像素还原回数据,这里有个简陋的 Demo

这样,既减少了宽带,也节省存储空间。

3 4 收藏 1 评论

相关文章

可能感兴趣的话题



直接登录
最新评论
跳到底部
返回顶部