前言
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 原封不動的傳輸。
演示
綜上所述,我們簡單演示下: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 有略微的提升,壓縮率仍是最高的。生成如下圖片:
結論
由于 canvas 導出圖片時,無法設置壓縮等級,而默認的壓縮率并不高。所以這種方式,最終效果并不理想。
同樣的數據,相比 Flash 壓縮,差距就很明顯了:
deflate 算法 | lzma 算法 | |
---|---|---|
體積 | 133,660 | 108,015 |
比率 | 21.0% | 17.0% |
并且 Flash 生成的是通用格式,后端解壓時,使用標準庫即可;而 PNG 還得位圖解碼、像素處理等步驟,很麻煩。
所以,現實中還是優先使用 Flash,本文只是開腦洞而已。
用例
雖然是個然并卵的黑科技,不過實際還是有用到過,曾用在一個較大日志上傳的場合(并且不能用 Flash)。
好在后端僅僅儲存而已,并不分析。所以,可以讓管理員將日志對應的 PNG 圖片下回本地,在自己電腦上解析。
解壓更容易,就是將像素還原回數據,這里有個簡陋的 Demo。
這樣,既減少了上傳流量,也節省服務器存儲空間。
文章列表