如何製作 QR Code #2:基本編碼模式

帶你實現屬於自己的 QR Code 產生器和解碼器

Yeecy
14 min readAug 21, 2020

QR Code 的標準《ISO/IEC 18004:2015》(後以標準稱之)規定了多種編碼方式,在本文中,我們將會探討四種基本的編碼方式。

點此閱讀《如何製作 QR Code》其他文章

在進入正題前,Yeecy 先稍微介紹一下進位制,以供不熟悉的讀者參考,往後我們將會因情景不同而使用不同的進位制。

進位制

我們日常生活中所用的數字,多是採用十進位表示,事實上,進位制只是一種數字的表示方法,就像蘋果和 apple 指的是同個水果,只是用的語言不同而已。

在前面提到,黑色的碼元代表 1,白色的碼元代表 0,並且也只有黑白兩種碼元,這表示 QR Code 使用二進位制來表示上面儲存的資料。在二進位制的世界裡我們只會看到 0 和 1,下面列出一些十進位與二進位表示的數字。

二進位  0   1  10  11  100 ... 1010十進位  0   1   2   3   4  ...  10

我們用 0b (b 表 binary)當作二進位數字的的前綴,如果數字沒有前綴,通常表示十進位數字,這種表示習慣適用於整個系列文。

要如何把十進位的數字轉換成二進位呢?方法很簡單,13 在十進位制中,表示 1×10¹ + 3×10⁰,其中 10 表示進位制的基底,而前面的係數拿出來寫在一起就會得到 13 。

同理,我們知道二進位制的基底是 2,也就是 0b10,我們只要將 13 改成以 2 為基底的形式,把係數拿出來寫在一起,就能得到十進位 13 在二進位的形式,也就是:

13 = 1×8 + 1×4 + 1×1 = 1×2³ + 1×2² + 0×2¹ + 1×2⁰ = 0b1101

至於二進位轉十進位的方法就把上面的式子從右往左看就好。

另外,八進位(octal,以 0o 為前綴)和十六進位(hexadecimal,以 0x 為前綴)也是常用的進位方法,雖然看起來很可怕,不過就像一些免洗手游一樣,這兩種進位方法也只是換皮的二進位。

舉上面的 0b1101 為例,我們從右向左,每三位為一組,數字不夠就補 0,得到 001 101,由於 001 = 2⁰ = 1,以及 101 = 2² + 2⁰ = 5,可以得到 0b1101 在八進位的形式 0o15 。

為何每三位一組呢?因為 8 可以表達 0 到 7,換成二進位是 0b000 到 0b111,所以當我們知道一個數的二進位表示時,就可以很輕鬆地知道八進位的表示囉!同理,每四位一組就能得到十六進位的表示,事實上,在已知二進位的表示時,每 N 個一組就能轉換成 2 的 N 次進位表示。

那麼如果我們不知道一個數的二進位表示,要怎麼換成八進位呢?其實方法跟十進位怎麼轉二進位一樣,把基底換成 8 後整理一下,再把係數合在一起寫出來就行。

這裡提供一些進位轉換的範例,供讀者參考。

23 = 0b10111 = 0o27 = 0x17
17 = 0b10001 = 0o21 = 0x11

順帶一提,Python 內建 bin()oct()hex() ,方便使用者轉換進位制。

>>> bin(10)        # decimal to binary
'0b1010'
>>> oct(10) # decimal to octal
'0o12'
>>> hex(10) # decimal to hexadecimal
'0xa'
>>> int('10', 2) # binary to decimal
2
>>> int('10', 8) # octal to decimal
8
>>> int('10', 16) # hexadecimal to decimal
16

數字模式(numeric mode)

在數字模式下,只有 0 到 9 可以編碼,畢竟都叫數字模式了,對吧?

編碼

我們現在假設欲編碼訊息為:

0123456789

由於輸入訊息都是數字,因此我們能夠使用數字模式來編碼,首先我們由左到右,每三個數字一組,將輸入訊息分組:

012、345、678、9

接下來把分好的數字分別轉換成長度為 10 的二進位數,如果最後一組數字只有兩個,把它轉換成長度為 7 的二進位數;如果只有一個,把它轉換成長度為 4 的二進位數:

0000001100、0101011001、1010100110、1001

再將四組數字併起來得到:

0000001100010101100110101001101001

如此一來,我們完成了數字模式的編碼。

解碼

看完如何編碼,相信有聰明的讀者已經猜到了怎麼解碼了,那就是把上述的步驟反著做就可以,假設欲解碼訊息如下:

00011110110101101

我們先從左到右,十個數字一組把它拆分開來:

0001111011、0101101

由最後一組數字的長度為 7 可以推得它代表了兩個數字,將它們轉換為十進位表示,可以得到:

123、45

接下來把它們併起來,得到原始訊息:

12345

就這樣,解碼完成囉!以下範例供讀者參考。

 input: 0
encode: 0000
decode: 0
input: 1
encode: 0001
decode: 1
input: 12303
encode: 00011110110000011
decode: 12303
input: 01917875412
encode: 0000010011001011001010111100100001100
decode: 01917875412

文數字模式(alphanumeric mode)

文數字模式中,共包含 45 個字元,字元表如下:

0: 00   9: 09   I: 18   R: 27    : 36
1: 01 A: 10 J: 19 S: 28 $: 37
2: 02 B: 11 K: 20 T: 29 %: 38
3: 03 C: 12 L: 21 U: 30 *: 39
4: 04 D: 13 M: 22 V: 31 +: 40
5: 05 E: 14 N: 23 W: 32 -: 41
6: 06 F: 15 O: 24 X: 33 .: 42
7: 07 G: 16 P: 25 Y: 34 /: 43
8: 08 H: 17 Q: 26 Z: 35 :: 44

由於 Medium 沒有提供表格,只能用手打的方式呈現,還請讀者多擔待。觀察字元表可以發現其中的英文字母都是大寫,所以小寫字母不能用此模式編碼。另外,第 36 個字元為空白字元。

如果要寫成程式,我建議把上表直接寫進程式碼中。

編碼

假設欲編碼訊息如下:

YEECY

我們先把每個字元拿去查表,得到對應的十進位值:

34、14、14、12、34

接下來由左到右,兩個數字為一組分開:

34、14 | 14、12 | 34

先關注第一組數字,我們現在把 34 和 14 當作是四十五進位的表示,也就是說這一組數字代表:

34×45¹ + 14×45⁰

接著把這組數字轉換成長度 11 的二進位表示(可以先換成十進位,再轉換成二進位),得到:

11000001000

我們對第二組做一樣的操作,如果今天最後一組只有一個數字,那就直接把它換成長度為 6 的二進位表示,依此方法,可以得到三個二進位數:

11000001000、01010000010、100010

把這三個數併起來,即得到編碼結果:

1100000100001010000010100010

解碼

道理跟前面提過的一樣,把編碼反著做就可以還原出原始的訊息,我們假設欲解碼訊息為:

00111001101001100

從左至右,每 11 個數字一組分開,可以得到:

00111001101、001100

從分開的結果可以觀察到原始訊息有三個字元。

現在我們看第一組數字,因為先前是將兩個字元看做是四十五進位的表示,而現在的數字是二進位的表示,先把它變回十進位:

0b00111001101 = 461

接著,把 461 變回四十五進位的表示:

461 = 10×45¹ + 11×45⁰

其中的 10 和 11 就是第一組原始字元的十進位值,同樣把第二組數字變回四十五進位的表示(即 12),綜合起來得到:

10、11、12

反查表,可以得到解碼結果為:

ABC

以下提供些許範例供讀者參考,冒號後的空白僅為排版之用。

 input: /O.O/
encode: 1111010011111101111010101011
decode: /O.O/
input: HELLO WORLD
encode: 0110000101101111000110100010111001011011100010011010100001101
decode: HELLO WORLD
input: ISO/IEC 18004:2015
encode: 011010001101000110001101100111000010010000000000011010100000000000000111000000000101101000000110010
decode: ISO/IEC 18004:2015
input: APTX4869
encode: 00111011011101001110100001011110000100010111
decode: APTX4869

位元組模式(byte mode)

在標準文件裡面規定,位元組模式採用 ISO/IEC 8859-1 編碼,但是有些 QR Code 產生器對此模式的實現採用 UTF-8 編碼,以取得對更多字元的支援,不過此處我們跟著標準走,UTF-8 編碼就留給讀者自行研究。

由於 Medium 不好呈現表格,ISO/IEC 8859-1的字元表請參考維基百科的 Code page layout

編碼

位元組模式的編碼相當簡單,假設欲編碼訊息為:

Byte¡

首先對每個字元查表,得到值為:

0x42、0x79、0x74、0x65、0xA1

接著把這些十六進位表示轉成長度為 8 的二進位表示:

01000010、01111001、01110100、01100101、10100001

把這些數字串起來就得到編碼結果:

0100001001111001011101000110010110100001

解碼

將欲解碼訊息以八位一組拆開,反查表即可得到原始訊息,此處不再贅述。

以下範例供讀者參考,冒號後空白僅為排版之用。

 input: QR Code
encode: 01010001010100100010000001000011011011110110010001100101
decode: QR Code
input: Yeecy
encode: 0101100101100101011001010110001101111001
decode: Yeecy
input: Hello, world! «(°ö° )¬
encode: 01001000011001010110110001101100011011110010110000100000011101110110111101110010011011000110010000100001001000001010101100101000101100001111011010110000001000000010100110101100
decode: Hello, world! «(°ö° )¬

漢字模式(Kanji mode)

根據標準,漢字模式採 Shift JIS 編碼,不過只涵蓋其中的 0x8140 到 0x9FFC 和 0xE040 到 0xEBBF,編碼表請參考 Shift JIS Kanji Code Table

順道提一下這個表要怎麼看,舉r為例,首先尋找r的位置,不難發現它位於 82 90 列,82 90 表示那一列第一個字元的值為 0x8290,接著計算r與該列第一個字元的距離,可以得到 2,我們把 0x8290 加上 2 即可得到 r 的十六進位值 0x8292。

另外再提一點,就我的觀察,該表中的字元都是全形的,所以半形字元沒辦法也不該用此模式編碼。

編碼

既然是能用漢字的編碼,那就文藝一點,令欲編碼訊息為:

蒹葭蒼蒼

首先我們查表得到每個字元所代表的值:

0xE4E3、0xE4D1、0x9193、0x9193

我們可以發現前兩個字元位在 0xE040 和 0xEBBF 間,而後面兩個字元位在 0x8140 和 0x9FFC 之間,針對兩個範圍的計算大抵相同,唯一不同的是需要減去的數,第一個範圍需減去 0xC140,而第二個範圍減去的是 0x8140。

舉 0xE4E3 為例,因為它位於 0xE040 和 0xEBBF 間,所以我們先把它減去 0xC140:

0xE4E3 − 0xC140 = 0x23A3

接著把 0x23A3 切成兩個部分:

0x23A3 → 0x23、0xA3

接著把上面的兩個數字當成一百九十二進位(即以 0xC0 為基底)表示,即:

0x23×192¹ + 0xA3×192⁰

我們把上面結果轉成十進位表示:

6883

最後,轉換成長度為 13 的二進位表示:

1101011100011

如此我們把「蒹」成功轉成 1101011100011。

接下來處理位於 0x8140 和 0x9FFC 間的「蒼」,首先將其減去 0x8140:

0x9193 − 0x8140 = 0x1053

接著把 0x1053 切成兩個部分:

0x1053 → 0x10、0x53

接著把上面的兩個數字當成一百九十二進位(即以 0xC0 為基底)表示,即:

0x10×192¹ + 0x53×192⁰

我們把上面結果轉成十進位表示:

3155

最後,轉換成長度為 13 的二進位表示:

0110001010011

如此一來,「蒼」也被成功轉換了。

依此方法,把「葭」也處理完後,可以得到編碼結果:

1101011100011110101101000101100010100110110001010011

解碼

假設欲解碼訊息為:

00111001111110101000011010

我們先將其分為 13 個數字一組:

0011100111111、0101000011010

把兩組數字化成十進位表示:

1855、2586

接著再轉成一百九十二進位的形式:

9×192¹ + 127×192⁰、13×192¹ + 90×192⁰

把係數以十六進位呈現:

0x09×192¹ + 0x7F×192⁰、0x0D×192¹ + 0x5A×192⁰

接著把係數合起來:

0x097F、0x0D5A

根據我的觀察,如果是數字開頭是 0,表示原始字元位於 0x8140 到 0x9FFC;如果數字開頭是 1,表示原始字元位於 0xE040 到 0xEBBF。

我們可以看到兩個原始字元都位在 0x8140 到 0x9FFC,所以把兩個數字都加上 0x8140,可以得到:

0x8ABF、0x8E9A

對上面兩個數字反查表,即可得到原始訊息:

漢字

以下範例供讀者參考,冒號後空白僅供排版使用,輸入訊息皆以全形字元呈現。

 input: ABC 012
encode: 0000011100000000001110000100000111000100000000000000000001100111100000110100000000011010001
decode: ABC\u3000012
input: こんにちは世界
encode: 0000100110001000010111000100001010010010000100111111000010100110101011101000100011011000101
decode: こんにちは世界
input: 色褪せてゆく 二人の記憶の中 今僕らは
encode: 0101101000110110111011001000001001110010000101000100000010110010000001001011010000000000000011100011000101011011011000000101001100001111000110000110011011110000101001100011010000011000000000000000100101100001011111110110000001011001110000101001101
decode: 色褪せてゆく\u3000二人の記憶の中\u3000今僕らは

註:\u3000 為 Unicode 中的全形空白

結語

我沒想到原來寫教學文是這麼費時的事情,打完這篇花了十五小時左右,希望讀者能明白這四種編碼方式,若想了解其他進階的編碼,請讀者自行翻閱標準。

至於用 Python 實現編解碼的讀者,可以善用 str.encode()bytes.decode() ,它們可以幫助你節省大量時間。

下一篇文章將會討論如何根據原始訊息和容錯等級,來得到進行加入錯誤校正前的資料編碼。

感謝你的閱讀,我是 Yeecy,我們下次見。

--

--

Yeecy

"What I cannot create, I do not understand. Know how to solve every problem that has been solved." - Richard Feynman