網(wǎng)絡(luò)上已經(jīng)有非常多的二維碼編碼和解碼工具和代碼,很多都是服務(wù)器端的,也就是說需要一臺服務(wù)器才能提供二維碼的生成。本著對服務(wù)器性能的考慮,這種小事情都讓服務(wù)器去做,感覺對不住服務(wù)器,尤其是對于大流量的網(wǎng)站,雖然有服務(wù)器端緩存,畢竟需要大量的CPU運(yùn)算時間,這或多或少也是很大的一塊壓力。所以就想,有沒有一種不靠服務(wù)器,就只靠JS就生成二維碼呢,畢竟二維碼就是一堆黑白點(diǎn)而已。我也沒有刻意去找網(wǎng)絡(luò)上是否已經(jīng)存在這樣的解決方案,而且自己一直想深入分析二維碼的生成細(xì)節(jié),現(xiàn)有的項(xiàng)目也有這樣的需求,于是我自己研究了下,寫下了這么個qr.js。
大家可以從這個地址下載:https://files.cnblogs.com/JerryWeng/qr.js
先看看這個東西的效果:
它有兩種輸出模式:
第一種是直接通過<img>對于base64的支持,把二維碼數(shù)據(jù)轉(zhuǎn)成一個bmp編碼的base64數(shù)據(jù)字符串作為<img>的src:
第二種是把每個點(diǎn)做成一個div,然后通過css變成一個黑白點(diǎn)的矩陣
這是測試的html代碼:
<!DOCTYPE html>
<html>
<head>
<script src="./jquery-1.11.1.min.js" type="text/JAVAscript"></script>
<script src="./qr.js" type="text/JavaScript"></script>
<script type="text/javascript">
var qr_coder = null;
$(document).ready(function(){
qr_coder = new QRCoder($('#qr_container'));
$('#qr_gen').click(function()
{
$('#qr_container').html("generating");
var watch_start=new Date();
qr_coder.setMode(1);
qr_coder.draw(
$('#qr_link').val(),
$("[name='qr_capacity']:checked").val(),
'icon.png',
function(data)
{
var watch_end=new Date();
console.log("cost:"+(watch_end-watch_start)+"ms");
});
});
});
</script>
</head>
<body>
<h1>QR CODER</h1>
<div style="margin:auto; position:relative; margin-left: 50%; left: -250px; width:500px;">
<label for="qr_link">URL:</label>
<input id="qr_link" type="text" value="http://you.ctrip.com" style="width:350px;" />
<button id="qr_gen" value="Generate">Generate</button> <br />
<div style="display:none">
<input id="qr_capacity_l" name="qr_capacity" type="radio" value="L"/> <label for="qr_capacity_l">7%</label>
<input id="qr_capacity_m" name="qr_capacity" type="radio" value="M"/> <label for="qr_capacity_m">15%</label>
<input id="qr_capacity_q" name="qr_capacity" type="radio" value="Q"/> <label for="qr_capacity_q">25%</label>
<input id="qr_capacity_h" name="qr_capacity" type="radio" value="H" checked/> <label for="qr_capacity_h">30%</label>
</div>
</div>
<div id="qr_container" style="margin:auto; position:relative;"></div>
</body>
</html>
在IE6,7,8,9,10,F(xiàn)irefox,Chrome中測試通過。
如果對于實(shí)現(xiàn)細(xì)節(jié)感興趣,下面我來詳細(xì)說明如何實(shí)現(xiàn)。
一、參考文檔
在開始之前,需要準(zhǔn)備一些參考文檔來幫助理解:
1, QR 國際標(biāo)準(zhǔn) ISO/IEC 18004. (http://raidenii.NET/files/datasheets/misc/qr_code.pdf)
2, http://coolshell.cn/articles/10590.html
3, Galois Field 伽羅華域 (參考度娘)
4, Reed Solomon 糾錯編碼 (參考度娘)
5, Bitmap 編碼規(guī)范 (http://zh.wikipedia.org/wiki/Bitmap)
6, Base64 編碼 (參考度娘)
二、流程
http://www.processon.com/view/link/537c20340cf27a0d78936e61
整個流程,步驟有點(diǎn)多,但其實(shí)并不復(fù)雜,其中大多數(shù)步驟在標(biāo)準(zhǔn)規(guī)范中已經(jīng)說明,在參考文檔2中,他已經(jīng)把編碼部分說的非常詳細(xì),我就不多贅述了,我在下面補(bǔ)充說下一些比較搞的概念。
三、說明
首先是伽羅華域,QR的糾錯編碼都是基于GF(256)的,GF的最大特性是它的封閉性,無論是加減乘除,它計(jì)算結(jié)果始終落在這個有限域中,并且GF256中的任何一個元素,都可以用GF2的組合來表示,也就是0,1表示,我們通過1+x^1+x^2+...+x^n這樣的多項(xiàng)式來表示一個這個有限域中的數(shù),其實(shí),我們不用在意這里的x,我們只關(guān)心這個多項(xiàng)式的系數(shù)組合,每個x的指數(shù)代表系數(shù)所占的位數(shù),比如x^8+x^6+x+1就對應(yīng)二進(jìn)制10100011,所以其實(shí)都是二進(jìn)制的運(yùn)算。GF256一共就256個數(shù),我們可以生成好,然后以數(shù)組和哈希表的形式來參與計(jì)算,具體如何生成GF256的,大家可以參考下這篇wiki,http://en.wikipedia.org/wiki/Finite_field_arithmetic
然后是RS糾錯編碼,RS編碼都是基于GF256的,所以,我們需要先熟悉GF256的運(yùn)算方法,RS編碼說簡單了,就是首先知道我需要有多少個糾錯的codeblock,然后以這個數(shù)構(gòu)造一個生成多項(xiàng)式:(x-a^0)(x-a^1)...(x-a^n-1),這里的a,或叫alpha,就是GF256里的底數(shù),a^n-1代表一個GF256有限域中元素,這里的n就是糾錯codeblock的個數(shù),然后把要編碼的數(shù)據(jù)codeblocks組成一個類似的多項(xiàng)式,每個codeblock的值就是多項(xiàng)式的系數(shù),從高位到低位排列,用這個數(shù)據(jù)多項(xiàng)式除以生成多項(xiàng)式,然后取余數(shù),這個余數(shù)也應(yīng)該是在GF256里的數(shù),其實(shí)就是手工法取余,這些運(yùn)算方法在GF的那篇wiki里也有說明,詳細(xì)也可參見這篇wiki:http://en.wikipedia.org/wiki/Reed–Solomon_error_correction
再說下mask的問題,最后編碼后的數(shù)據(jù),為了能夠盡量地分散黑點(diǎn)和白點(diǎn)的分布,便于掃描器掃描,需要每個數(shù)據(jù)位與某種mask做XOR,為什么不是固定的mask呢,因?yàn)闆]法用一種mask分散所有的編碼。規(guī)范中列舉了8種mask函數(shù),這些函數(shù),只要符合,就返回1,否則是0,然后每個對應(yīng)的數(shù)據(jù)位(x,y)代入這個函數(shù),然后再和相應(yīng)的數(shù)據(jù)位XOR,這里的x代表列號,y代表行號,左上角是0點(diǎn),規(guī)范中的i代表的是行號,j代表的是列號,這點(diǎn)要注意。然后我們要從8個mask函數(shù)中選擇一個最合適的,選擇方法是分別和4種決策方法并根據(jù)其權(quán)重計(jì)算一個分?jǐn)?shù)并求和,選取這個得分最低的mask就是我們要用的mask。這4種決策方法和權(quán)重在規(guī)范中有列舉,稍微看下,不難理解。其實(shí)這部操作也是最耗性能的,因?yàn)楸仨氁?*4次計(jì)算,而且每次計(jì)算要掃描整個數(shù)據(jù)陣列。其實(shí)前3種決策方法算起來還都好,最麻煩的是最后種,要計(jì)算m*n同色塊,每次出現(xiàn)需要加(m-1)*(n-1)*3,這個計(jì)算我沒有找到一個比較理想的算法,我變通的做法是,只計(jì)算出現(xiàn)機(jī)率最多的小塊矩形,2<=m<6,2<=n<6的共16種矩形,其實(shí)結(jié)果計(jì)算的差不了多少。其實(shí)不是說沒有算對就完全掃不出來,這個選取操作可以讓生成的二維碼最優(yōu)化而已。這個操作在客戶端大概在百ms級別的,其實(shí)用戶是感受不到它的生成過程,但是如果這個操作放在服務(wù)器端,可想而知壓力之大。
然后說下生成Bitmap,位圖。因?yàn)橹挥?個顏色,所以用1個bit的位圖,在不壓縮的情況下,也不會很大。這里有一點(diǎn)需要注意的是,位圖的布局方式是最后一行先寫,然后依次向上,而且每一行的總字節(jié)數(shù),必須是4的倍數(shù),比如一個version3的qr碼,是33*33個像素陣,一行33個像素要33個bits,5個bytes,但是在輸出的時候,必須加上3個bytes來湊滿8=4*2個bytes,有點(diǎn)惡心,但其實(shí)大小還是可控的。
最后說下嵌入logo的問題,因?yàn)镼R有強(qiáng)力的RS糾錯編碼,所以,一個小圖片放在中間也不會影響掃描器掃描,但是需要一個較高的糾錯等級,我這邊只是把這個圖片作為一個浮動層飄在二維碼上面,當(dāng)然也是可以把它嵌入到剛才提到的bitmap中,但是太復(fù)雜了,意義也不大,暫且就這樣了。
關(guān)于詳細(xì)的實(shí)現(xiàn)細(xì)節(jié)和使用方法,在qr.js里我已經(jīng)非常詳細(xì)地注解了,時間倉促,肯能會有bug,見諒!
======UPDATE 2014-05-22====
有人反應(yīng)IE6和7是不支持base64圖片的,為了避免針對IE6-7的簡單兼容,我將qr.js稍作修改,當(dāng)選擇mode 0且是ie6 或者 ie7,將強(qiáng)制選擇mode 1做輸出。
PS:base64圖片在IE6,7下可以使用mht的組合文件形式輸出,我試了下,不是非常靈活,建議還是在mode 1下輸出。當(dāng)時未能用真實(shí)的低版本環(huán)境測試而是直接用模擬版本,實(shí)在抱歉!JS就是瀏覽器兼容性非常惡心!
======UPDATE 2014-05-23====
URL最好不要以斜杠'/'結(jié)尾,很多掃描器無法打開這樣的鏈接。我試了下,有些網(wǎng)站最后帶/是可以打開的,比如http://www.baidu.com/,所以也很難說是掃描器的問題。我測試了以下一些站點(diǎn):
http://www.google.com/
http://www.baidu.com/
http://www.microsoft.com/
以上這些網(wǎng)站帶/都可以掃描出來
http://you.ctrip.com/
http://www.163.com/
http://www.cnblogs.com/
以上這些網(wǎng)站帶/都掃描不出來
然后大家感興趣的話可以去比較下,用Fiddler抓下包,然后看下請求Header和返回Header,不難發(fā)現(xiàn),請求Header都是差不多的,區(qū)別在于返回的header,所有帶/且不能掃描出來的返回請求中,都對Content-Encoding做了定義,gzip或者chunked,所以,我覺得可能是手機(jī)瀏覽器在解碼并處理默認(rèn)跳轉(zhuǎn)的時候無法獲得默認(rèn)的跳轉(zhuǎn)內(nèi)容頁,所以導(dǎo)致無法顯示掃描出來的頁面。如果不帶斜杠,服務(wù)器端找到默認(rèn)路徑和默認(rèn)頁并輸出到緩沖區(qū),如果帶/,客戶端直接定位到默認(rèn)路徑。






