天天看點

Javascript生成二維碼(QR)

網絡上已經有非常多的二維碼編碼和解碼工具和代碼,很多都是伺服器端的,也就是說需要一台伺服器才能提供二維碼的生成。本着對伺服器性能的考慮,這種小事情都讓伺服器去做,感覺對不住伺服器,尤其是對于大流量的網站,雖然有伺服器端緩存,畢竟需要大量的CPU運算時間,這或多或少也是很大的一塊壓力。是以就想,有沒有一種不靠伺服器,就隻靠JS就生成二維碼呢,畢竟二維碼就是一堆黑白點而已。我也沒有刻意去找網絡上是否已經存在這樣的解決方案,而且自己一直想深入分析二維碼的生成細節,現有的項目也有這樣的需求,于是我自己研究了下,寫下了這麼個qr.js。

大家可以從這個位址下載下傳:https://files.cnblogs.com/JerryWeng/qr.js

先看看這個東西的效果:

Javascript生成二維碼(QR)

它有兩種輸出模式:

第一種是直接通過<img>對于base64的支援,把二維碼資料轉成一個bmp編碼的base64資料字元串作為<img>的src:

Javascript生成二維碼(QR)
第二種是把每個點做成一個div,然後通過css變成一個黑白點的矩陣
Javascript生成二維碼(QR)
這是測試的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,Firefox,Chrome中測試通過。

如果對于實作細節感興趣,下面我來詳細說明如何實作。

一、參考文檔

在開始之前,需要準備一些參考文檔來幫助了解:

1, QR 國際标準 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 編碼規範 (http://zh.wikipedia.org/wiki/Bitmap)

6, Base64 編碼 (參考度娘)

二、流程

http://www.processon.com/view/link/537c20340cf27a0d78936e61

Javascript生成二維碼(QR)

整個流程,步驟有點多,但其實并不複雜,其中大多數步驟在标準規範中已經說明,在參考文檔2中,他已經把編碼部分說的非常詳細,我就不多贅述了,我在下面補充說下一些比較搞的概念。

三、說明

首先是伽羅華域,QR的糾錯編碼都是基于GF(256)的,GF的最大特性是它的封閉性,無論是加減乘除,它計算結果始終落在這個有限域中,并且GF256中的任何一個元素,都可以用GF2的組合來表示,也就是0,1表示,我們通過1+x^1+x^2+...+x^n這樣的多項式來表示一個這個有限域中的數,其實,我們不用在意這裡的x,我們隻關心這個多項式的系數組合,每個x的指數代表系數所占的位數,比如x^8+x^6+x+1就對應二進制10100011,是以其實都是二進制的運算。GF256一共就256個數,我們可以生成好,然後以數組和哈希表的形式來參與計算,具體如何生成GF256的,大家可以參考下這篇wiki,http://en.wikipedia.org/wiki/Finite_field_arithmetic

然後是RS糾錯編碼,RS編碼都是基于GF256的,是以,我們需要先熟悉GF256的運算方法,RS編碼說簡單了,就是首先知道我需要有多少個糾錯的codeblock,然後以這個數構造一個生成多項式:(x-a^0)(x-a^1)...(x-a^n-1),這裡的a,或叫alpha,就是GF256裡的底數,a^n-1代表一個GF256有限域中元素,這裡的n就是糾錯codeblock的個數,然後把要編碼的資料codeblocks組成一個類似的多項式,每個codeblock的值就是多項式的系數,從高位到低位排列,用這個資料多項式除以生成多項式,然後取餘數,這個餘數也應該是在GF256裡的數,其實就是手工法取餘,這些運算方法在GF的那篇wiki裡也有說明,詳細也可參見這篇wiki:http://en.wikipedia.org/wiki/Reed–Solomon_error_correction

再說下mask的問題,最後編碼後的資料,為了能夠盡量地分散黑點和白點的分布,便于掃描器掃描,需要每個資料位與某種mask做XOR,為什麼不是固定的mask呢,因為沒法用一種mask分散所有的編碼。規範中列舉了8種mask函數,這些函數,隻要符合,就傳回1,否則是0,然後每個對應的資料位(x,y)代入這個函數,然後再和相應的資料位XOR,這裡的x代表列号,y代表行号,左上角是0點,規範中的i代表的是行号,j代表的是列号,這點要注意。然後我們要從8個mask函數中選擇一個最合适的,選擇方法是分别和4種決策方法并根據其權重計算一個分數并求和,選取這個得分最低的mask就是我們要用的mask。這4種決策方法和權重在規範中有列舉,稍微看下,不難了解。其實這部操作也是最耗性能的,因為必須要做8*4次計算,而且每次計算要掃描整個資料陣列。其實前3種決策方法算起來還都好,最麻煩的是最後種,要計算m*n同色塊,每次出現需要加(m-1)*(n-1)*3,這個計算我沒有找到一個比較理想的算法,我變通的做法是,隻計算出現機率最多的小塊矩形,2<=m<6,2<=n<6的共16種矩形,其實結果計算的差不了多少。其實不是說沒有算對就完全掃不出來,這個選取操作可以讓生成的二維碼最優化而已。這個操作在用戶端大概在百ms級别的,其實使用者是感受不到它的生成過程,但是如果這個操作放在伺服器端,可想而知壓力之大。

然後說下生成Bitmap,位圖。因為隻有2個顔色,是以用1個bit的位圖,在不壓縮的情況下,也不會很大。這裡有一點需要注意的是,位圖的布局方式是最後一行先寫,然後依次向上,而且每一行的總位元組數,必須是4的倍數,比如一個version3的qr碼,是33*33個像素陣,一行33個像素要33個bits,5個bytes,但是在輸出的時候,必須加上3個bytes來湊滿8=4*2個bytes,有點惡心,但其實大小還是可控的。

最後說下嵌入logo的問題,因為QR有強力的RS糾錯編碼,是以,一個小圖檔放在中間也不會影響掃描器掃描,但是需要一個較高的糾錯等級,我這邊隻是把這個圖檔作為一個浮動層飄在二維碼上面,當然也是可以把它嵌入到剛才提到的bitmap中,但是太複雜了,意義也不大,暫且就這樣了。

關于詳細的實作細節和使用方法,在qr.js裡我已經非常詳細地注解了,時間倉促,肯能會有bug,見諒!

======UPDATE 2014-05-22====

有人反應IE6和7是不支援base64圖檔的,為了避免針對IE6-7的簡單相容,我将qr.js稍作修改,當選擇mode 0且是ie6 或者 ie7,将強制選擇mode 1做輸出。

PS:base64圖檔在IE6,7下可以使用mht的組合檔案形式輸出,我試了下,不是非常靈活,建議還是在mode 1下輸出。當時未能用真實的低版本環境測試而是直接用模拟版本,實在抱歉!JS就是浏覽器相容性非常惡心!

======UPDATE 2014-05-23====

URL最好不要以斜杠'/'結尾,很多掃描器無法打開這樣的連結。我試了下,有些網站最後帶/是可以打開的,比如http://www.baidu.com/,是以也很難說是掃描器的問題。我測試了以下一些站點:

http://www.google.com/

http://www.baidu.com/

http://www.microsoft.com/

以上這些網站帶/都可以掃描出來

http://you.ctrip.com/

http://www.163.com/

http://www.cnblogs.com/

以上這些網站帶/都掃描不出來

然後大家感興趣的話可以去比較下,用Fiddler抓下包,然後看下請求Header和傳回Header,不難發現,請求Header都是差不多的,差別在于傳回的header,所有帶/且不能掃描出來的傳回請求中,都對Content-Encoding做了定義,gzip或者chunked,是以,我覺得可能是手機浏覽器在解碼并處理預設跳轉的時候無法獲得預設的跳轉内容頁,是以導緻無法顯示掃描出來的頁面。如果不帶斜杠,伺服器端找到預設路徑和預設頁并輸出到緩沖區,如果帶/,用戶端直接定位到預設路徑。

繼續閱讀