看到二維碼,很容易猜到黑白相間的小方格就是二進制比特。那麼這些比特是怎麼獲得的?小方格又是按照什麼規則排布的?今天我們就從零開始將一個 url 畫成二維碼。html
考慮到大多數人可能不太瞭解二維碼,因此先講下基礎概念。你也能夠先看看左耳朵耗子寫的二維碼的生成細節和原理。node
二維碼一共有 40 個尺寸,官方叫做版本 Version。最小的 Version 1 是 21 × 21 的矩陣,Version 2 是 25 × 25 的矩陣 … 每增長 1 version,就增長 4 的尺寸。最高是 177 × 177 的矩陣(Version 40)。算法
根據平常使用經驗,不管是殘缺、遮擋仍是污損,二維碼都可以被正確識別,這得益於二維碼的糾錯機制。shell
糾錯是經過糾錯碼實現的,當咱們將源數據編碼後,會採用一種糾錯算法算出編碼結果對應的糾錯碼,連同編碼結果一塊兒填充到二維碼。編碼
規範裏明確了四個糾錯等級,不一樣等級的糾錯比例不一樣。url
每一個版本的糾錯碼個數都是固定的,好比 version 1-L 須要 7 個糾錯碼,version 1-M 須要 10 個糾錯碼,版本越大、數據越多、等級越高,須要的糾錯碼就越多。pwa
二維碼能夠簡單劃分爲三個部分:固定的功能區(用於定位)、固定的格式信息區(好比二維碼用的糾錯等級和版本信息)、數據區。code
二進制數據都是嚴格按 8 位一組填入數據區的,每一組稱爲一個 codeword。若是數據區的格子數不是 8 的倍數,就會剩下留白(remainder)。每一個版本的功能區、格式信息區和留白的位置及其佔用的面積都是固定的。cdn
更詳細的劃分能夠參考規範裏的這張圖:htm
理論上凡是能夠被編譯爲二進制的數據,均可以用二維碼來表示,只要有相應的解碼程序便可。規範裏規定了幾種國際通用的編碼模式:
好比 Numeric 僅支持 0 ~ 9 的數字,它的編碼規則是:將數據字符串拆分爲每三個一組,每組做爲一個十進制的百位數,而後轉化爲 10 位二進制。爲何是 10 位呢,由於 2^10 = 1024,剛好能覆蓋最大的百位數 999。若是最後一組只有兩位,則轉化爲 7 位二進制,若是隻有一位,則轉化爲 4 位二進制。
Alphanumeric 支持 45 個字符,0 ~ 九、A ~ Z 和幾個特殊字符。
Alphanumeric 的編碼規則是:根據上表查出每一個 char 對應的 value,而後兩兩分組,按 A * 45 + B 算出一個值,並將這個值轉化爲 11 位二進制。若是最後一組只有一位,則將 value 轉化爲 6 位二進制。
Byte 模式的編碼規則最簡單,就是針對單字節字符集(例如 ISO-8859-1)的編碼,每一個字符剛好用 8 個比特表示。
咱們還能夠針對不一樣的數據片斷採用不一樣的編碼模式,這就是混合模式。
二維碼包含的二進制串具體是由如下幾個部分組成的:
最開頭必定是編碼模式,是固定的 4 位(見編碼一節的表格)。
接下來是原字符的長度,不一樣版本和不一樣編碼模式用多少個比特表示長度是不同的,具體是幾個,規範裏有明確的規定。畢竟二維碼要在有限的空間裏容納更多的信息,就不能浪費比特,用過長的二進制去表示一個較小的值。
而後就是對數據按照指定的編碼模式編譯出的二進制序列。若是以上三部分的數據長度不是 8 的倍數,那麼必須添零湊整(terminator)。
上述數據不見得能填滿整個數據區,那麼剩餘的空間就用兩個補齊碼(11101100、00010001)交錯填滿。這兩個補齊碼是規範指定的。
以上整個比特串做爲輸入,用糾錯算法輸出特定個數的糾錯碼,兩者相接,就是整個二維碼數據區的數據了。
------------------ 分割線 ------------------
接下來,我們就選用最小的版本,和最低的糾錯等級,用 Alphanumeric 模式將 I.MEITUAN.COM 這個字符串畫成一個二維碼。
查表能夠獲得每一個字符對應的碼點,而後兩兩分組,恰好分紅 10 組:(17, 29)(29, 25)(44, 43)(43, 18)(42, 22)(14, 18)(29, 30)(10, 23)(42, 12)(24, 22)。
按照 A * 45 + B 算出:794, 1330, 2023, 1953, 1912, 648, 1335, 473, 1902, 1102。
而後將每一個數字依次轉化爲二進制(toString(2)):01100011010 10100110010 11111100111 11110100001 11101111000 01010001000 10100110111 00111011001 11101101110 10001001110。
Alphanumeric 的 mode indicator 是 0010,在 version 1 裏須要 9 個比特表示長度(20 => 000010100),將它們添加到頭部:0010 000010100 01100011010 10100110010 11111100111 11110100001 11101111000 01010001000 10100110111 00111011001 11101101110 10001001110。
按 8 位一組重排,添 0 湊整:00100000 10100011 00011010 10100110 01011111 10011111 11010000 11110111 10000101 00010001 01001101 11001110 11001111 01101110 10001001 11000000。
version 1-L 共有 26 個 codeword,其中有 7 個糾錯碼(error correction codeword),那麼 data codeword 應該有 19 個,目前咱們只有 16 個,所以剩下 3 個須要用補齊碼湊足:00100000 10100011 00011010 10100110 01011111 10011111 11010000 11110111 10000101 00010001 01001101 11001110 11001111 01101110 10001001 11000000 11101100 00010001 11101100。
而後算出 7 個糾錯碼:11010100 01110000 01110111 10010001 00101110 10010101 11001111。(我是用 node-qrcode 反算出來的,搞不懂那個糾錯算法)。
最終咱們獲得了整個編碼結果:
這一步最簡單,按照規範裏的規定畫便可。爲了便於區分,用淡紫色表示功能區的白色區域。
將數據按 8 位一組,從右下角開始蛇形往復填入 2 × 4 的矩形。具體怎麼填規範裏有明確規定,upwards 方向,將第一個數填入位置 7,第二個數填入位置 6 ... 這裏是略有點坑的,由於不是從圖中所示的 0 到 7,而是反過來的。方便起見,咱們將數據倒過來,再按 0 ~ 7 的順序依次填入。
masking 的做用是使二維碼的分佈更加均勻。具體操做就是將數據區和 mask pattern 對應位置的比特作異或運算。好比數據區第 8 行第 10 列是白色 0,mask pattern 第 8 行第 10 列是黑色 1,異或運算的結果是黑色 1,那麼就將數據區第 8 行第 10 列改爲黑色。官方規定了八種 mask pattern(000 ~ 111),咱們選用 000。
對全部的版本,格式信息都是固定的 15 位。其中兩個是糾錯等級,三個是 mask pattern,剩下十個是糾錯碼。
能夠想象,沒有格式信息,根本沒法進行解碼。所以格式信息很是重要,會重複畫兩次(相似於雙機房備份)。
左下角那一列開頭是永遠固定的佔位符。
格式信息也須要經歷糾錯碼 + masking 的處理。
一樣的,應該反過來,從位置 14 開始填。固然,也能夠先把字符串倒過來。
最終結果: