文章出處

前言

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 * hh * 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

這樣,既減少了上傳流量,也節省服務器存儲空間。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()