【探索】利用 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 个字节:
R = bytes[0]
G = bytes[1]
B = bytes[2]
A = bytes[3]
事实上有现成的方法,可批量将数据填充成像素:
var img = new ImageData(bytes, w, h);
context.putImageData(img, 0, 0);
但是,图片的宽高如何设定?
尺寸
最简单的,就是用 1px 的高度。比如有 1000 个像素,则填在 1000 x 1 的图片里。
但如果有 10000 像素,就不可行了。因为 canvas 的尺寸,是有限制的。
不同的浏览器,最大尺寸不一样。有 4096 的,也有 32767 的。。。
以最大 4096 为例,如果每次都用这个宽度,显然不合理。
比如有 n = 4100 个像素,我们使用 4096 x 2 的尺寸:
| 1 | 2 | 3 | 4 | ... | 4095 | 4096 |
| 4097 | 4098 | 4099 | 4100 | ...... 未利用 ......
第二行只用到 4 个,剩下的 4092 个都空着了。
但 4100 = 41 * 100。如果用这个尺寸,就不会有浪费。
所以,得对 n 分解因数:
n = w * h
这样就能将 n 个像素,正好填满 w x h 的图片。
但 n 是质数的话,就无解了。这时浪费就不可避免了,只是,怎样才能浪费最少?
于是就变成这样一个问题:
如何用 n + m 个点,拼成一个矩形。求矩形的 w 和 h。(n 已知,m 越小越好,0 < w <= MAX, 0 < h <= MAX)
考虑到 MAX 不大,穷举就可以。
我们遍历 h,计算相应的 w = ceil(n / h), 然后找出最接近 n 的 w * h。
var MAX = 4096;
var beg = Math.ceil(n / MAX);
var end = Math.ceil(Math.sqrt(n));
var minSize = 9e9;
var bestH = 0, // 最终结果
bestW = 0;
for (h = beg; h <= end; h++) {
var w = Math.ceil(n / h);
var size = w * h;
if (size < minSize) {
minSize = size;
bestW = w;
bestH = h;
}
if (size == n) {
break;
}
}
因为 w * h 和 h * w 是一样的,所以只需遍历到 sqrt(n) 就可以。
同样,也无需从 1 开始,从 n / MAX 即可。
这样,我们就能找到最适合的图片尺寸。
当然,连续的空白像素,最终压缩后会很小。这一步其实并不特别重要。
渲染
定下尺寸,我们就可以「渲染数据」了。
渲染看似简单,然而事实上却有个意想不到的坑 —— 同个像素写入后再读取,数据居然会有偏差!这里有个测试:
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
// 写入的数据
var bytes = [100, 101, 102, 103];
var buf = new Uint8ClampedArray(bytes);
var img = new ImageData(buf, 1, 1);
ctx.putImageData(img, 0, 0);
// 读取的数据
img = ctx.getImageData(0, 0, 1, 1);
console.log(img.data);
// 期望 [100, 101, 102, 103]
// 实际
// chrome [99, 102, 102, 103]
// firefox [101, 101, 103, 103]
// ...
读取的值和写入的很接近,但并不相同。而且不同的浏览器,偏差还不一样!这究竟是怎么回事?
原来,浏览器为了提高渲染性能,有一个 Premultiplied Alpha 的机制。但是,这会牺牲一些精度!虽然视觉上并不明显,但用于数据存储,就有问题了。
如何禁用它?一番尝试都没成功。于是,只能从数据上琢磨。如果不使用 Alpha 通道,又会怎样?
// 写入的数据
var bytes = [100, 101, 102, 255];
...
console.log(img.data); // [100, 101, 102, 255]
设置 A = 255,这样倒是避开了问题。
看来,只能从数据上着手,跳过 Alpha 通道:
// pixel 1
new_bytes[0] = bytes[0] // R
new_bytes[1] = bytes[1] // G
new_bytes[2] = bytes[2] // B
new_bytes[3] = 255 // A
// pixel 2
new_bytes[4] = bytes[3] // R
new_bytes[5] = bytes[4] // G
new_bytes[6] = bytes[5] // B
new_bytes[7] = 255 // A
...
这时,就不受 Premultiplied Alpha 的影响了。
出于简单,也可以 1 像素存 1 字节:
// pixel 1
new_bytes[0] = bytes[0]
new_bytes[1] = 255
new_bytes[2] = 255
new_bytes[3] = 255
// pixel 2
new_bytes[4] = bytes[1]
new_bytes[5] = 255
new_bytes[6] = 255
new_bytes[7] = 255
...
这样,整个图片最多只有 256 色。如果能导出成「索引型 PNG」的话,也是可以尝试的。
解码
最后,就是将图像导出成可传输的数据。如果 canvas 能直接导出成 blob,那是最好的,因为 blob 可通过 AJAX 上传。
canvas.toBlob(function(blob) {
// ...
}, 'image/png')
不过,大多浏览器都不支持,只能导出 data uri 格式:
uri = canvas.toDataURL('image/png') // data:image/png;base64,xxxx
然而 base64 会增加 1/3 的长度,这样压缩效果就大幅降低了。所以,我们还得解码成二进制:
base64 = uri.substr(uri.indexOf(',') + 1)
binary = atob(base64)
这时的 binary,就是最终想要的数据了吗?如果将 binary 通过 AJAX 提交的话,会发现实际传输字节,会比 binary.length 大!
原来 atob 函数返回的数据,仍是字符串型的,所以传输时会涉及到字集编码。因此我们还需再转换一次,变成真正的二进制类型:
var len = binary.length
var buf = new Uint8Array(len)
for (var i = 0; i < len; i++) {
buf[i] = binary.charCodeAt(i)
}
这时的 buf,才能被 AJAX 原封不动的传输。
演示
综上所述,我们简单演示下:https://www.etherdream.com/FunnyScript/jszip/encode.html
找一个大块的文本测试。例如 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 有略微的提升,压缩率仍是最高的。生成如下图片:

结论
由于 canvas 导出图片时,无法设置压缩等级,而默认的压缩率并不高。所以这种方式,最终效果并不理想。
同样的数据,相比 Flash 压缩,差距就很明显了:
| deflate 算法 | lzma 算法 | |
|---|---|---|
| 体积 | 133,660 | 108,015 |
| 比率 | 21.0% | 17.0% |
并且 Flash 生成的是通用格式,后端解压时,使用标准库即可;而 PNG 还得位图解码、像素处理等步骤,很麻烦。
所以,现实中还是优先使用 Flash,本文只是开脑洞而已。
用例
虽然是个然并卵的黑科技,不过实际还是有用到过,曾用在一个较大日志上传的场合(并且不能用 Flash)。
好在后端仅仅储存而已,并不分析。所以,可以让管理员将日志对应的 PNG 图片下回本地,在自己电脑上解析。
解压更容易,就是将像素还原回数据,这里有个简陋的 Demo:https://www.etherdream.com/FunnyScript/jszip/decode.html。
这样,既减少了上传流量,也节省服务器存储空间。
【探索】利用 canvas 实现数据压缩的更多相关文章
- 利用 canvas 破解 某拖动验证码
利用 canvas 破解 某拖动验证码 http://my.oschina.net/u/237940/blog/337194
- 利用Canvas进行绘制XY坐标系
首先来一发图 绘制XY的坐标主要是利用Canvas setLeft和setBottom功能(Canvas内置坐标的功能) 1.首先WPF中的坐标系都是从左到右,从上到下的 即左上角位置(0,0)点,所 ...
- 利用canvas实现的中点Bresenham算法
Bresenham提出的直线生成算法的基本原理是,每次在最大位移方向上走一步,而另一个方向是走步还是不走步取决于误差项的判别,具体的实现过程大家可以去问度娘.我主要是利用canvas画布技术实现了这个 ...
- 利用canvas压缩图片
现在手机拍的照片动不动就是几M,当用户上传手机里的照片时一个消耗流量大,一个上传时间长,为了解决这个问题,就需要压缩图片: 想法:利用canvas重绘图片,保持宽高比不变,具体宽高根本具体情况而定. ...
- HTML5利用canvas,把多张图合并成一张图片
需求分析,根据当前网页中的几张图片,在手机上长按,保存图片到相册或者发送给好友. drawCanvas(){ var self = this; var imgsrcArray = [ require( ...
- 利用canvas将网页元素生成图片并保存在本地
利用canvas将网页元素生成图片并保存在本地 首先引入三个文件: 1.<script type="text/javascript" src="js/html2ca ...
- 利用canvas对上传图片进行上传前压缩
利用谷歌调式工具发现,图片大小直接影响着首屏加载时间. 且考虑到后期服务端压力,图片压缩特别必要. 本文是前端利用canvas实现图片.参考文章:https://www.cnblogs.com/007 ...
- 10分钟,利用canvas画一个小的loading界面
首先利用定义下canvas得样式 <canvas width="1024" height="720" id="canvas" styl ...
- 利用canvas和RGraph作图
利用canvas可以直接在页面中绘制各种复杂的图形,其中引用到一个Rgraph的插件. Rgraph插件使用非常方便,只需几步就可以完成一个折线图.饼图.柱状图,或是其中两者图形的结合! (1) 引用 ...
随机推荐
- 谈谈一些有趣的CSS题目(十一)-- reset.css 知多少?
开本系列,谈谈一些有趣的 CSS 题目,题目类型天马行空,想到什么说什么,不仅为了拓宽一下解决问题的思路,更涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题 ...
- 消息队列 Kafka 的基本知识及 .NET Core 客户端
前言 最新项目中要用到消息队列来做消息的传输,之所以选着 Kafka 是因为要配合其他 java 项目中,所以就对 Kafka 了解了一下,也算是做个笔记吧. 本篇不谈论 Kafka 和其他的一些消息 ...
- redis集成到Springmvc中及使用实例
redis是现在主流的缓存工具了,因为使用简单.高效且对服务器要求较小,用于大数据量下的缓存 spring也提供了对redis的支持: org.springframework.data.redis.c ...
- C++随笔:从Hello World 探秘CoreCLR的内部(1)
紧接着上次的问题,上次的问题其实很简单,就是HelloWorld.exe运行失败,而本文的目的,就是成功调试HelloWorld这个控制台应用程序. 通过我的寻找,其实是一个名为TryRun的文件出了 ...
- 趣说游戏AI开发:曼哈顿街角的A*算法
0x00 前言 请叫我标题党!请叫我标题党!请叫我标题党!因为下面的文字既不发生在美国曼哈顿,也不是一个讲述美国梦的故事.相反,这可能只是一篇没有那么枯燥的关于算法的文章.A星算法,这个在游戏寻路开发 ...
- .NET 基础 一步步 一幕幕[面向对象之对象和类]
对象和类 本篇正式进入面向对象的知识点简述: 何为对象,佛曰:一花一世界,一木一浮生,一草一天堂,一叶一如来,一砂一极乐,一方一净土,一笑一尘缘,一念一清静.可见"万物皆对象". ...
- 易用BPM时代,软件开发者缘何选择H3?
近年来,企业级软件开发市场暗流汹涌,呈现出多种态势.软件开发团队规模趋于小型化,工作方式趋于快捷化,超过半数的软件开发者在工作中会选择使用易用的软件开发工具.随着流程管理越来越受到企业的重视,流程开发 ...
- iOS之开发中常用的颜色及其对应的RGB值
R G B 值 R G B 值 R G B 值 黑色 0 0 0 #000000 黄色 255 255 0 #FFFF00 浅灰蓝色 176 224 230 #B0E0E6 象牙黑 41 ...
- Firebug中调试中的js脚本中中文内容显示为乱码
Firebug中调试中的js脚本中中文内容显示为乱码 设置 页面 UFT-8 编码没用, 解决方法:点击 "Firebug"工具栏 中的"选项"---" ...
- Linux初识
在这篇文章中你讲看到如下内容: 计算机的组成及功能: Linux发行版之间的区别和联系: Linux发行版的基础目录及功用规定: Linux系统设计的哲学思想: Linux系统上获取命令帮助,及man ...