利用 canvas 實現資料壓縮

發表於2016-03-15

前言

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

這樣,既減少了寬頻,也節省儲存空間。

相關文章