410 lines
11 KiB
Go
410 lines
11 KiB
Go
package qrcode
|
|
|
|
import (
|
|
"errors"
|
|
"log"
|
|
"strconv"
|
|
|
|
// "github.com/skip2/go-qrcode/bitset"
|
|
"github.com/yeqown/reedsolomon/binary"
|
|
)
|
|
|
|
// ecLevel error correction level
|
|
type ecLevel int
|
|
|
|
const (
|
|
// ErrorCorrectionLow :Level L: 7% error recovery.
|
|
ErrorCorrectionLow ecLevel = iota + 1
|
|
|
|
// ErrorCorrectionMedium :Level M: 15% error recovery. Good default choice.
|
|
ErrorCorrectionMedium
|
|
|
|
// ErrorCorrectionQuart :Level Q: 25% error recovery.
|
|
ErrorCorrectionQuart
|
|
|
|
// ErrorCorrectionHighest :Level H: 30% error recovery.
|
|
ErrorCorrectionHighest
|
|
|
|
formatInfoBitsNum = 15 // format info bits num
|
|
verInfoBitsNum = 18 // version info length bits num
|
|
)
|
|
|
|
var (
|
|
errInvalidErrorCorrectionLevel = errors.New("invalid error correction level")
|
|
errAnalyzeVersionFailed = errors.New("could not match version! " +
|
|
"check your content length is in limitation of encode mode and error correction level")
|
|
errMissMatchedVersion = errors.New("could not match version")
|
|
errMissMatchedEncodeType = errors.New("could not match the encode type")
|
|
// versions []version
|
|
// Each QR Code contains a 15-bit Format Information qrbool. The 15 bits
|
|
// consist of 5 data bits concatenated with 10 error correction bits.
|
|
//
|
|
// The 5 data bits consist of:
|
|
// - 2 bits for the error correction level (L=01, M=00, G=11, H=10).
|
|
// - 3 bits for the data mask pattern identifier.
|
|
//
|
|
// formatBitSequence is a mapping from the 5 data bits to the completed 15-bit
|
|
// Format Information qrbool.
|
|
//
|
|
// For example, a QR Code using error correction level L, and data mask
|
|
// pattern identifier 001:
|
|
//
|
|
// 01 | 001 = 01001 = 0x9
|
|
// formatBitSequence[0x9].qrCode = 0x72f3 = 111001011110011
|
|
formatBitSequence = []struct {
|
|
regular uint32
|
|
micro uint32
|
|
}{
|
|
{0x5412, 0x4445}, {0x5125, 0x4172}, {0x5e7c, 0x4e2b}, {0x5b4b, 0x4b1c},
|
|
{0x45f9, 0x55ae}, {0x40ce, 0x5099}, {0x4f97, 0x5fc0}, {0x4aa0, 0x5af7},
|
|
{0x77c4, 0x6793}, {0x72f3, 0x62a4}, {0x7daa, 0x6dfd}, {0x789d, 0x68ca},
|
|
{0x662f, 0x7678}, {0x6318, 0x734f}, {0x6c41, 0x7c16}, {0x6976, 0x7921},
|
|
{0x1689, 0x06de}, {0x13be, 0x03e9}, {0x1ce7, 0x0cb0}, {0x19d0, 0x0987},
|
|
{0x0762, 0x1735}, {0x0255, 0x1202}, {0x0d0c, 0x1d5b}, {0x083b, 0x186c},
|
|
{0x355f, 0x2508}, {0x3068, 0x203f}, {0x3f31, 0x2f66}, {0x3a06, 0x2a51},
|
|
{0x24b4, 0x34e3}, {0x2183, 0x31d4}, {0x2eda, 0x3e8d}, {0x2bed, 0x3bba},
|
|
}
|
|
|
|
// QR Codes version 7 and higher contain an 18-bit version Information qrbool,
|
|
// consisting of a 6 data bits and 12 error correction bits.
|
|
//
|
|
// versionBitSequence is a mapping from QR Code version to the completed
|
|
// 18-bit version Information qrbool.
|
|
//
|
|
// For example, a QR code of version 7:
|
|
// versionBitSequence[0x7] = 0x07c94 = 000111110010010100
|
|
versionBitSequence = []uint32{
|
|
0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x07c94,
|
|
0x085bc, 0x09a99, 0x0a4d3, 0x0bbf6, 0x0c762, 0x0d847, 0x0e60d, 0x0f928,
|
|
0x10b78, 0x1145d, 0x12a17, 0x13532, 0x149a6, 0x15683, 0x168c9, 0x177ec,
|
|
0x18ec4, 0x191e1, 0x1afab, 0x1b08e, 0x1cc1a, 0x1d33f, 0x1ed75, 0x1f250,
|
|
0x209d5, 0x216f0, 0x228ba, 0x2379f, 0x24b0b, 0x2542e, 0x26a64, 0x27541, 0x28c69,
|
|
}
|
|
)
|
|
|
|
// capacity struct includes data type max capacity
|
|
type capacity struct {
|
|
Numeric int `json:"n"` // num capacity
|
|
AlphaNumeric int `json:"a"` // char capacity
|
|
Byte int `json:"b"` // byte capacity (utf-8 also)
|
|
JP int `json:"j"` // Japanese capacity
|
|
}
|
|
|
|
// group contains fields to generate ECBlocks
|
|
// and append _defaultPadding bit
|
|
type group struct {
|
|
// NumBlocks num of blocks
|
|
NumBlocks int `json:"nbs"`
|
|
|
|
// NumDataCodewords Number of data codewords.
|
|
NumDataCodewords int `json:"ndcs"`
|
|
|
|
// ECBlockwordsPerBlock ...
|
|
ECBlockwordsPerBlock int `json:"ecbs_pb"`
|
|
}
|
|
|
|
// version ...
|
|
type version struct {
|
|
// version code 1-40
|
|
Ver int `json:"ver"`
|
|
|
|
// ECLevel error correction 0, 1, 2, 3
|
|
ECLevel ecLevel `json:"eclv"`
|
|
|
|
// Cap includes each type's max capacity (specified by `Ver` and `ecLevel`)
|
|
// ref to: https://www.thonky.com/qr-code-tutorial/character-capacities
|
|
Cap capacity `json:"cap"`
|
|
|
|
// RemainderBits remainder bits need to append finally
|
|
RemainderBits int `json:"rembits"`
|
|
|
|
// groups info to generate
|
|
// ref to: https://www.thonky.com/qr-code-tutorial/error-correction-table
|
|
// numGroup = len(Groups)
|
|
Groups []group `json:"groups"`
|
|
}
|
|
|
|
// Dimension ...
|
|
func (v version) Dimension() int {
|
|
return v.Ver*4 + 17
|
|
}
|
|
|
|
// NumTotalCodewords total data codewords
|
|
func (v version) NumTotalCodewords() int {
|
|
var total int
|
|
for _, g := range v.Groups {
|
|
total = total + (g.NumBlocks * g.NumDataCodewords)
|
|
}
|
|
return total
|
|
}
|
|
|
|
// NumGroups ... need group num. ref to version config file
|
|
func (v version) NumGroups() int {
|
|
return len(v.Groups)
|
|
}
|
|
|
|
// TotalNumBlocks ... total data blocks num, ref to version config file
|
|
func (v version) TotalNumBlocks() int {
|
|
var total int
|
|
for _, g := range v.Groups {
|
|
total = total + g.NumBlocks
|
|
}
|
|
return total
|
|
}
|
|
|
|
// VerInfo version info bitset
|
|
func (v version) verInfo() *binary.Binary {
|
|
if v.Ver < 7 {
|
|
return nil
|
|
}
|
|
|
|
result := binary.New()
|
|
result.AppendUint32(versionBitSequence[v.Ver], verInfoBitsNum)
|
|
|
|
return result
|
|
}
|
|
|
|
// formatInfo returns the 15-bit Format Information qrbool for a QR
|
|
// code.
|
|
func (v version) formatInfo(maskPattern int) *binary.Binary {
|
|
formatID := 0
|
|
|
|
switch v.ECLevel {
|
|
case ErrorCorrectionLow:
|
|
formatID = 0x08 // 0b01000
|
|
case ErrorCorrectionMedium:
|
|
formatID = 0x00 // 0b00000
|
|
case ErrorCorrectionQuart:
|
|
formatID = 0x18 // 0b11000
|
|
case ErrorCorrectionHighest:
|
|
formatID = 0x10 // 0b10000
|
|
default:
|
|
log.Panicf("Invalid level %d", v.ECLevel)
|
|
}
|
|
|
|
if maskPattern < 0 || maskPattern > 7 {
|
|
log.Panicf("Invalid maskPattern %d", maskPattern)
|
|
}
|
|
|
|
formatID |= maskPattern & 0x7
|
|
result := binary.New()
|
|
result.AppendUint32(formatBitSequence[formatID].regular, formatInfoBitsNum)
|
|
return result
|
|
}
|
|
|
|
var emptyVersion = version{Ver: -1}
|
|
|
|
// binarySearchVersion speed up searching target version in versions.
|
|
// low, high to set the left and right bound of the search range (min:0 to max:159).
|
|
// compare represents the function to compare the target version with the cursor version.
|
|
// negative means lower direction, positive means higher direction, zero mean hit.
|
|
func binarySearchVersion(low, high int, compare func(*version) int) (hit version, found bool) {
|
|
// left low and high in a valid range
|
|
if low > high || low > _VERSIONS_ITEM_COUNT || high < 0 {
|
|
return emptyVersion, false
|
|
}
|
|
|
|
if low < 0 {
|
|
low = 0
|
|
}
|
|
if high >= _VERSIONS_ITEM_COUNT {
|
|
high = len(versions) - 1
|
|
}
|
|
|
|
for low <= high {
|
|
mid := (low + high) / 2
|
|
r := compare(&versions[mid])
|
|
if r == 0 {
|
|
hit = versions[mid]
|
|
found = true
|
|
break
|
|
}
|
|
|
|
if r > 0 {
|
|
// move toward higher direction
|
|
low = mid + 1
|
|
} else {
|
|
// move toward lower direction
|
|
high = mid
|
|
}
|
|
}
|
|
|
|
return hit, found
|
|
}
|
|
|
|
// defaultBinaryCompare built-in compare function for binary search.
|
|
func defaultBinaryCompare(ver int, ec ecLevel) func(cursor *version) int {
|
|
return func(cursor *version) int {
|
|
switch r := ver - cursor.Ver; r {
|
|
case 0:
|
|
default:
|
|
// v is bigger return positive; otherwise return negative.
|
|
return r
|
|
}
|
|
|
|
return int(ec - cursor.ECLevel)
|
|
}
|
|
}
|
|
|
|
// loadVersion get version config by specified version indicator and error correction level.
|
|
// we can speed up this process, by shrink the range to search.
|
|
func loadVersion(lv int, ec ecLevel) version {
|
|
// each version only has 4 items in versions array,
|
|
// and them are ordered[ASC] already.
|
|
high := lv*4 - 1
|
|
low := (lv - 1) * 4
|
|
|
|
for i := low; i <= high; i++ {
|
|
if versions[i].ECLevel == ec {
|
|
return versions[i]
|
|
}
|
|
}
|
|
panic(errMissMatchedVersion)
|
|
}
|
|
|
|
// analyzeVersion the raw text, and then decide which version should be chosen
|
|
// according to the text length , error correction level and encode mode to choose the
|
|
// closest capacity of version.
|
|
//
|
|
// check out http://muyuchengfeng.xyz/%E4%BA%8C%E7%BB%B4%E7%A0%81-%E5%AD%97%E7%AC%A6%E5%AE%B9%E9%87%8F%E8%A1%A8/
|
|
// for more details.
|
|
func analyzeVersion(raw []byte, ec ecLevel, mode encMode) (*version, error) {
|
|
step := 0
|
|
switch ec {
|
|
case ErrorCorrectionLow:
|
|
step = 0
|
|
case ErrorCorrectionMedium:
|
|
step = 1
|
|
case ErrorCorrectionQuart:
|
|
step = 2
|
|
case ErrorCorrectionHighest:
|
|
step = 3
|
|
default:
|
|
return nil, errInvalidErrorCorrectionLevel
|
|
}
|
|
|
|
want, mark := len(raw), 0
|
|
for ; step < 160; step += 4 {
|
|
|
|
switch mode {
|
|
case EncModeNumeric:
|
|
mark = versions[step].Cap.Numeric
|
|
case EncModeAlphanumeric:
|
|
mark = versions[step].Cap.AlphaNumeric
|
|
case EncModeByte:
|
|
mark = versions[step].Cap.Byte
|
|
case EncModeJP:
|
|
mark = versions[step].Cap.JP
|
|
default:
|
|
return nil, errMissMatchedEncodeType
|
|
}
|
|
|
|
if mark >= want {
|
|
return &versions[step], nil
|
|
}
|
|
}
|
|
debugLogf("mismatched version, version's length: %d, ec: %v", len(versions), ec)
|
|
|
|
return nil, errAnalyzeVersionFailed
|
|
}
|
|
|
|
var (
|
|
// https://www.thonky.com/qr-code-tutorial/alignment-pattern-locations
|
|
// DONE(@yeqown): add more version
|
|
alignPatternLocation = map[int][]int{
|
|
2: {6, 18},
|
|
3: {6, 22},
|
|
4: {6, 26},
|
|
5: {6, 30},
|
|
6: {6, 34},
|
|
7: {6, 22, 38},
|
|
8: {6, 24, 42},
|
|
9: {6, 26, 46},
|
|
10: {6, 28, 50},
|
|
11: {6, 30, 54},
|
|
12: {6, 32, 58},
|
|
13: {6, 34, 62},
|
|
14: {6, 26, 46, 66},
|
|
15: {6, 26, 48, 70},
|
|
16: {6, 26, 50, 74},
|
|
17: {6, 30, 54, 78},
|
|
18: {6, 30, 56, 82},
|
|
19: {6, 30, 58, 86},
|
|
20: {6, 34, 62, 90},
|
|
21: {6, 28, 50, 72, 94},
|
|
22: {6, 26, 50, 74, 98},
|
|
23: {6, 30, 54, 78, 102},
|
|
24: {6, 28, 54, 80, 106},
|
|
25: {6, 32, 58, 84, 110},
|
|
26: {6, 30, 58, 86, 114},
|
|
27: {6, 34, 62, 90, 118},
|
|
28: {6, 26, 50, 74, 98, 122},
|
|
29: {6, 30, 54, 78, 102, 126},
|
|
30: {6, 26, 52, 78, 104, 130},
|
|
31: {6, 30, 56, 82, 108, 134},
|
|
32: {6, 34, 60, 86, 112, 138},
|
|
33: {6, 30, 58, 86, 114, 142},
|
|
34: {6, 34, 62, 90, 118, 146},
|
|
35: {6, 30, 54, 78, 102, 126, 150},
|
|
36: {6, 24, 50, 76, 102, 128, 154},
|
|
37: {6, 28, 54, 80, 106, 132, 158},
|
|
38: {6, 32, 58, 84, 110, 136, 162},
|
|
39: {6, 26, 54, 82, 110, 138, 166},
|
|
40: {6, 30, 58, 86, 114, 142, 170},
|
|
}
|
|
|
|
alignPatternCache = map[int][]loc{}
|
|
)
|
|
|
|
// loc point position(x,y)
|
|
type loc struct {
|
|
X int // for width
|
|
Y int // for height
|
|
}
|
|
|
|
// loadAlignmentPatternLoc ...
|
|
func loadAlignmentPatternLoc(ver int) (locs []loc) {
|
|
if ver < 2 {
|
|
return
|
|
}
|
|
var ok bool
|
|
if locs, ok = alignPatternCache[ver]; ok {
|
|
return
|
|
}
|
|
|
|
dimension := ver*4 + 17
|
|
positions, ok := alignPatternLocation[ver]
|
|
if !ok {
|
|
panic("could not found align at version: " + strconv.Itoa(ver))
|
|
}
|
|
|
|
for _, pos1 := range positions {
|
|
for _, pos2 := range positions {
|
|
if !valid(pos1, pos2, dimension) {
|
|
continue
|
|
}
|
|
locs = append(locs, loc{X: pos1, Y: pos2})
|
|
}
|
|
}
|
|
alignPatternCache[ver] = locs
|
|
return
|
|
}
|
|
|
|
// x, y center position x,y so
|
|
func valid(x, y, dimension int) bool {
|
|
// valid left-top
|
|
if (x-2) < 7 && (y-2) < 7 {
|
|
return false
|
|
}
|
|
// valid right-top
|
|
if (x+2) > dimension-7 && (y-2) < 7 {
|
|
return false
|
|
}
|
|
// valid left-bottom
|
|
if (x-2) < 7 && (y+2) > dimension-7 {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|