diff --git a/debug.go b/debug.go index f734ab9..1f8b9f6 100644 --- a/debug.go +++ b/debug.go @@ -83,7 +83,7 @@ func debugDrawTo(w io.Writer, mat matrix.Matrix) error { // background rectangle(0, 0, width, height, img, color.White) - mat.Iterate(matrix.ROW, func(x int, y int, v matrix.State) { + mat.Iterate(matrix.COLUMN, func(x int, y int, v matrix.State) { sx := x*blockWidth + padding sy := y*blockWidth + padding es := (x+1)*blockWidth + padding diff --git a/mask.go b/mask.go index 501a4bc..dd4f38f 100644 --- a/mask.go +++ b/mask.go @@ -27,202 +27,6 @@ const ( modulo7 ) -// calculateScore calculate the maskScore of masking result ... -func calculateScore(mat *matrix.Matrix) int { - debugLogf("calculate maskScore starting") - score1 := rule1(mat.Copy()) - score2 := rule2(mat.Copy()) - score3 := rule3(mat.Copy()) - score4 := rule4(mat.Copy()) - - debugLogf("maskScore: %d", score1+score2+score3+score4) - return score1 + score2 + score3 + score4 -} - -// 第一条规则为一行(或列)中的每组五个或更多相同颜色的模块提供QR代码。 -func rule1(mat *matrix.Matrix) int { - // Row socre - var ( - score int - rowCurState matrix.State - rowCurColorCnt int - - colCurState matrix.State - colCurColorCnt int - ) - - mat.Iterate(matrix.ROW, func(x, y int, value matrix.State) { - if x == 0 { - rowCurColorCnt = 0 - rowCurState = value - return - } - - if value == rowCurState { - rowCurColorCnt++ - } else { - rowCurState = value - } - - if rowCurColorCnt == 5 { - score += 3 - } else if rowCurColorCnt > 5 { - score++ - } - }) - - // column - mat.Iterate(matrix.COLUMN, func(x, y int, value matrix.State) { - if x == 0 { - colCurColorCnt = 0 - colCurState = value - return - } - - if value == colCurState { - colCurColorCnt++ - } else { - colCurState = value - } - - if colCurColorCnt == 5 { - score += 3 - } else if colCurColorCnt > 5 { - score++ - } - }) - return score -} - -// 第二个规则给出了QR码对矩阵中相同颜色模块的每个2x2区域的惩罚。 -func rule2(mat *matrix.Matrix) int { - var ( - score int - s0, s1, s2, s3 matrix.State - ) - for x := 0; x < mat.Width()-1; x++ { - for y := 0; y < mat.Height()-1; y++ { - s0, _ = mat.Get(x, y) - s1, _ = mat.Get(x+1, y) - s2, _ = mat.Get(x, y+1) - s3, _ = mat.Get(x+1, y+1) - - if s0 == s1 && s2 == s3 && s1 == s2 { - score += 3 - } - } - } - - return score -} - -// 如果存在看起来类似于取景器模式的模式,则第三规则给QR码一个大的惩罚 -// dark-light-dark-dark-dark-light-dark -// 1011101 0000 or 0000 1011101 -//func rule3_backup(mat *matrix.Matrix) (score int) { -// for y := 0; y < mat.Height(); y++ { -// for x := 0; x < mat.Width()-11; x++ { -// stateSlice := make([]matrix.State, 0, 11) -// for i := 0; i < 11; i++ { -// s, _ := mat.Get(x+i, y) -// stateSlice = append(stateSlice, s) -// } -// if matrix.StateSliceMatched(statePattern1, stateSlice) { -// score += 40 -// } -// if matrix.StateSliceMatched(statePattern2, stateSlice) { -// score += 40 -// } -// } -// } -// -// for x := 0; x < mat.Width(); x++ { -// for y := 0; y < mat.Height()-11; y++ { -// stateSlice := make([]matrix.State, 0, 11) -// for i := 0; i < 11; i++ { -// s, _ := mat.Get(x, y+i) -// stateSlice = append(stateSlice, s) -// } -// if matrix.StateSliceMatched(statePattern1, stateSlice) { -// score += 40 -// } -// if matrix.StateSliceMatched(statePattern2, stateSlice) { -// score += 40 -// } -// } -// } -// -// return score -//} - -// rule3 calculate punishment score in rule3, find pattern in QR Code matrix. -func rule3(mat *matrix.Matrix) (score int) { - var ( - pattern1 = binaryToStateSlice("1011101 0000") - pattern2 = binaryToStateSlice("0000 1011101") - pattern1Next = kmpGetNext(pattern1) - pattern2Next = kmpGetNext(pattern2) - ) - - // prerequisites: - // - // mat.Width() == mat.Height() - if mat.Width() != mat.Height() { - debugLogf("rule3 got matrix but not matched prerequisites") - } - dimension := mat.Width() - - for i := 0; i < dimension; i++ { - col := mat.Col(i) - row := mat.Row(i) - - // DONE(@yeqown): statePattern1 and statePattern2 are fixed, so maybe kmpGetNext - // could cache result to speed up. - score += 40 * kmp(col, pattern1, pattern1Next) - score += 40 * kmp(col, pattern2, pattern2Next) - score += 40 * kmp(row, pattern1, pattern1Next) - score += 40 * kmp(row, pattern2, pattern2Next) - } - - return score -} - -// 如果超过一半的模块是暗的或轻的,则第四规则给QR码一个惩罚,对较大的差异有较大的惩罚 -func rule4(mat *matrix.Matrix) int { - var ( - totalCnt = mat.Width() * mat.Height() - darkCnt, darkPercent int - ) - mat.Iterate(matrix.ROW, func(x, y int, s matrix.State) { - if s == matrix.StateTrue { - darkCnt++ - } - }) - darkPercent = (darkCnt * 100) / totalCnt - x := 0 - if darkPercent%5 == 0 { - x = 1 - } - last5Times := abs(((darkPercent/5)-x)*5 - 50) - next5Times := abs(((darkPercent/5)+1)*5 - 50) - - // get the min maskScore - if last5Times > next5Times { - // scoreC <- next5Times / 5 * 10 - return next5Times * 2 - } else { - return last5Times * 2 - } - -} - -func abs(x int) int { - if x < 0 { - return -x - } - return x -} - type mask struct { mat *matrix.Matrix // matrix mode maskPatternModulo // mode @@ -275,7 +79,7 @@ func (m *mask) masking() { panic("impossible panic, contact maintainer plz") } - m.mat.Iterate(matrix.ROW, func(x, y int, s matrix.State) { + m.mat.Iterate(matrix.COLUMN, func(x, y int, s matrix.State) { // skip the function modules if state, _ := m.mat.Get(x, y); state != matrix.StateInit { _ = m.mat.Set(x, y, matrix.StateInit) @@ -336,18 +140,3 @@ func modulo6Func(x, y int) bool { func modulo7Func(x, y int) bool { return ((x+y)%2+(x*y)%3)%2 == 0 } - -func binaryToStateSlice(s string) []matrix.State { - var states = make([]matrix.State, 0, len(s)) - for _, c := range s { - switch c { - case '1': - states = append(states, matrix.StateTrue) - case '0': - states = append(states, matrix.StateFalse) - default: - continue - } - } - return states -} diff --git a/mask_evaluation.go b/mask_evaluation.go new file mode 100644 index 0000000..fa1f3b1 --- /dev/null +++ b/mask_evaluation.go @@ -0,0 +1,174 @@ +package qrcode + +import ( + "math" + + "github.com/yeqown/go-qrcode/v2/matrix" +) + +// evaluation calculate a score after masking matrix. +// +// reference: +// - https://www.thonky.com/qr-code-tutorial/data-masking#Determining-the-Best-Mask +func evaluation(mat *matrix.Matrix) (score int) { + debugLogf("calculate maskScore starting") + + score1 := rule1(mat) + score2 := rule2(mat) + score3 := rule3(mat) + score4 := rule4(mat) + score = score1 + score2 + score3 + score4 + debugLogf("maskScore: rule1=%d, rule2=%d, rule3=%d, rule4=%d", score1, score2, score3, score4) + + return score +} + +// check each row one-by-one. If there are five consecutive modules of the same color, +// add 3 to the penalty. If there are more modules of the same color after the first five, +// add 1 for each additional module of the same color. Afterward, check each column one-by-one, +// checking for the same condition. Add the horizontal and vertical total to obtain penalty score +func rule1(mat *matrix.Matrix) (score int) { + // prerequisites: + // mat.Width() == mat.Height() + if mat.Width() != mat.Height() { + debugLogf("matrix width != height, skip rule1") + return math.MaxInt + } + + dimension := mat.Width() + scoreLine := func(arr []matrix.State) int { + lineScore, cnt, curState := 0, 0, matrix.StateInit + for _, v := range arr { + if !samestate(v, curState) { + curState = v + cnt = 1 + continue + } + + cnt++ + if cnt == 5 { + lineScore += 3 + } else if cnt > 5 { + lineScore++ + } + } + + return lineScore + } + + for cur := 0; cur < dimension; cur++ { + row := mat.Row(cur) + col := mat.Col(cur) + score += scoreLine(row) + score += scoreLine(col) + } + + return score +} + +// rule2 +// look for areas of the same color that are at least 2x2 modules or larger. +// The QR code specification says that for a solid-color block of size m × n, +// the penalty score is 3 × (m - 1) × (n - 1). +func rule2(mat *matrix.Matrix) int { + var ( + score int + s0, s1, s2, s3 matrix.State + ) + for x := 0; x < mat.Width()-1; x++ { + for y := 0; y < mat.Height()-1; y++ { + s0, _ = mat.Get(x, y) + s1, _ = mat.Get(x+1, y) + s2, _ = mat.Get(x, y+1) + s3, _ = mat.Get(x+1, y+1) + + if s0 == s1 && s2 == s3 && s1 == s2 { + score += 3 + } + } + } + + return score +} + +// rule3 calculate punishment score in rule3, find pattern in QR Code matrix. +// Looks for patterns of dark-light-dark-dark-dark-light-dark that have four +// light modules on either side. In other words, it looks for any of the +// following two patterns: 1011101 0000 or 0000 1011101. +// +// Each time this pattern is found, add 40 to the penalty score. +func rule3(mat *matrix.Matrix) (score int) { + var ( + pattern1 = binaryToStateSlice("1011101 0000") + pattern2 = binaryToStateSlice("0000 1011101") + pattern1Next = kmpGetNext(pattern1) + pattern2Next = kmpGetNext(pattern2) + ) + + // prerequisites: + // + // mat.Width() == mat.Height() + if mat.Width() != mat.Height() { + debugLogf("rule3 got matrix but not matched prerequisites") + return math.MaxInt + } + dimension := mat.Width() + + for i := 0; i < dimension; i++ { + col := mat.Col(i) + row := mat.Row(i) + + // DONE(@yeqown): statePattern1 and statePattern2 are fixed, so maybe kmpGetNext + // could cache result to speed up. + score += 40 * kmp(col, pattern1, pattern1Next) + score += 40 * kmp(col, pattern2, pattern2Next) + score += 40 * kmp(row, pattern1, pattern1Next) + score += 40 * kmp(row, pattern2, pattern2Next) + } + + return score +} + +// rule4 is based on the ratio of light modules to dark modules: +// +// 1. Count the total number of modules in the matrix. +// 2. Count how many dark modules there are in the matrix. +// 3. Calculate the percent of modules in the matrix that are dark: (darkmodules / totalmodules) * 100 +// 4. Determine the previous and next multiple of five of this percent. +// 5. Subtract 50 from each of these multiples of five and take the absolute value of the result. +// 6. Divide each of these by five. For example, 10/5 = 2 and 5/5 = 1. +// 7. Finally, take the smallest of the two numbers and multiply it by 10. +// +func rule4(mat *matrix.Matrix) int { + // prerequisites: + // + // mat.Width() == mat.Height() + if mat.Width() != mat.Height() { + debugLogf("rule4 got matrix but not matched prerequisites") + return math.MaxInt + } + + dimension := mat.Width() + dark, total := 0, dimension*dimension + for i := 0; i < dimension; i++ { + col := mat.Col(i) + + // count dark modules + for j := 0; j < dimension; j++ { + if samestate(col[j], matrix.StateTrue) { + dark++ + } + } + } + + ratio := (dark * 100) / total // in range [0, 100] + step := 0 + if ratio%5 == 0 { + step = 1 + } + + previous := abs((ratio/5-step)*5 - 50) + next := abs((ratio/5+1-step)*5 - 50) + + return min(previous, next) / 5 * 10 +} diff --git a/mask_evaluation_test.go b/mask_evaluation_test.go new file mode 100644 index 0000000..02cf168 --- /dev/null +++ b/mask_evaluation_test.go @@ -0,0 +1,56 @@ +package qrcode + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +//func Test_rule3_refactor(t *testing.T) { +// qrc, err := New("baidu.com google.com qq.com sina.com apple.com") +// assert.NoError(t, err) +// _ = qrc +// old := rule3_backup(qrc.mat) +// refactor := rule3(qrc.mat) +// assert.Equal(t, old, refactor) +//} + +func Benchmark_rule3(b *testing.B) { + qrc, err := New("baidu.com google.com qq.com sina.com apple.com") + assert.NoError(b, err) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = rule3(qrc.mat) + } +} + +//func Test_rule1_refactor(t *testing.T) { +// qrc, err := New("baidu.com google.com qq.com sina.com apple.com") +// assert.NoError(t, err) +// qrc.mat.Print() +// +// old := rule1_backup(qrc.mat) +// refactor := rule1(qrc.mat) +// assert.Equal(t, old, refactor) +//} + +func Benchmark_rule1(b *testing.B) { + qrc, err := New("baidu.com google.com qq.com sina.com apple.com") + assert.NoError(b, err) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = rule1(qrc.mat) + } +} + +//func Test_rule4_refactor(t *testing.T) { +// qrc, err := New("baidu.com google.com qq.com sina.com apple.com") +// assert.NoError(t, err) +// //qrc.mat.Print() +// +// old := rule4_backup(qrc.mat) +// refactor := rule4(qrc.mat) +// assert.Equal(t, old, refactor) +//} diff --git a/mask_test.go b/mask_test.go index 37cc7d0..c2b26a2 100644 --- a/mask_test.go +++ b/mask_test.go @@ -3,8 +3,6 @@ package qrcode import ( "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/yeqown/go-qrcode/v2/matrix" @@ -19,7 +17,7 @@ func TestMask(t *testing.T) { require.NoError(t, err) var stateInitCnt int - qrc.mat.Iterate(matrix.ROW, func(x, y int, s matrix.State) { + qrc.mat.Iterate(matrix.COLUMN, func(x, y int, s matrix.State) { if s == matrix.StateInit { stateInitCnt++ } @@ -54,22 +52,3 @@ func TestMask(t *testing.T) { mask7 := newMask(cpyMat, modulo7) _ = debugDraw("./assets/modulo7.jpeg", *mask7.mat) } - -//func Test_rule3_refactor(t *testing.T) { -// qrc, err := New("baidu.com google.com qq.com sina.com apple.com") -// assert.NoError(t, err) -// _ = qrc -// old := rule3_backup(qrc.mat) -// refactor := rule3(qrc.mat) -// assert.Equal(t, old, refactor) -//} - -func Benchmark_rule3(b *testing.B) { - qrc, err := New("baidu.com google.com qq.com sina.com apple.com") - assert.NoError(b, err) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = rule3(qrc.mat) - } -} diff --git a/matrix/matrix.go b/matrix/matrix.go index 1a6f2c7..ddbafc0 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -106,7 +106,7 @@ func (m *Matrix) init() { // Print to stdout func (m *Matrix) print() { m.Iterate(ROW, func(x, y int, s State) { - fmt.Printf("%6d ", s) + fmt.Printf("%2d ", s) if (x + 1) == m.width { fmt.Println() } @@ -119,18 +119,19 @@ func (m *Matrix) Print() { // Copy matrix into a new Matrix func (m *Matrix) Copy() *Matrix { - newMat := make([][]State, m.width) + mat2 := make([][]State, m.width) for w := 0; w < m.width; w++ { - newMat[w] = make([]State, m.height) - copy(newMat[w], m.mat[w]) + mat2[w] = make([]State, m.height) + copy(mat2[w], m.mat[w]) } - newM := &Matrix{ + m2 := &Matrix{ width: m.width, height: m.height, - mat: newMat, + mat: mat2, } - return newM + + return m2 } // Width ... width @@ -169,7 +170,8 @@ func (m *Matrix) Get(w, h int) (State, error) { // IterateFunc ... type IterateFunc func(int, int, State) -// Iterate the Matrix with loop direction ROW major or COLUMN major +// Iterate the Matrix with loop direction ROW major or COLUMN major. +// COLUMN is recommended. func (m *Matrix) Iterate(dir ScanDirection, f IterateFunc) { // row direction first if dir == ROW { diff --git a/qrcode.go b/qrcode.go index a1e3fff..3f45895 100644 --- a/qrcode.go +++ b/qrcode.go @@ -608,19 +608,20 @@ func (q *QRCode) masking() { dimension := q.v.Dimension() + // fill bitset into matrix + cpy := q.mat.Copy() + q.fillDataBinary(cpy, dimension) + // init mask and mats for i := 0; i < 8; i++ { masks[i] = newMask(q.mat, maskPatternModulo(i)) - mats[i] = q.mat.Copy() + mats[i] = cpy.Copy() } // generate 8 matrix with mask for i := 0; i < 8; i++ { wg.Add(1) go func(i int) { - // fill bitset into matrix - q.fillDataBinary(mats[i], dimension) - _ = debugDraw(fmt.Sprintf("draft/mats_%d.jpeg", i), *mats[i]) _ = debugDraw(fmt.Sprintf("draft/mask_%d.jpeg", i), *masks[i].mat) @@ -637,7 +638,7 @@ func (q *QRCode) masking() { } // calculate score and decide the lowest score and Draw - score := calculateScore(mats[i]) + score := evaluation(mats[i]) debugLogf("cur idx: %d, score: %d, current lowest: mats[%d]:%d", i, score, markMatsIdx, lowScore) scoreChan <- maskScore{ Score: score, @@ -663,9 +664,9 @@ func (q *QRCode) masking() { q.mat = mats[markMatsIdx] } -// all mask patter and check the maskScore choose the the lowest mask result +// all mask patter and check the maskScore choose the lowest mask result func (q *QRCode) xorMask(m *matrix.Matrix, mask *mask) { - mask.mat.Iterate(matrix.ROW, func(x, y int, s matrix.State) { + mask.mat.Iterate(matrix.COLUMN, func(x, y int, s matrix.State) { // skip the empty place if s == matrix.StateInit { return diff --git a/utilities.go b/utilities.go new file mode 100644 index 0000000..eaae500 --- /dev/null +++ b/utilities.go @@ -0,0 +1,53 @@ +package qrcode + +import "github.com/yeqown/go-qrcode/v2/matrix" + +// samestate judge two matrix State is same with binary semantic. +// StateFalse/StateInit only equal to StateFalse, other state are equal to each other. +func samestate(s1, s2 matrix.State) bool { + if s1 == s2 { + return true + } + + switch s1 { + case matrix.StateFalse, matrix.StateInit: + return false + } + switch s2 { + case matrix.StateFalse, matrix.StateInit: + return false + } + + return true +} + +func abs(x int) int { + if x < 0 { + return -x + } + + return x +} + +func min(x, y int) int { + if x < y { + return x + } + + return y +} + +func binaryToStateSlice(s string) []matrix.State { + var states = make([]matrix.State, 0, len(s)) + for _, c := range s { + switch c { + case '1': + states = append(states, matrix.StateTrue) + case '0': + states = append(states, matrix.StateFalse) + default: + continue + } + } + return states +} diff --git a/utilities_test.go b/utilities_test.go new file mode 100644 index 0000000..abc8eee --- /dev/null +++ b/utilities_test.go @@ -0,0 +1,203 @@ +package qrcode + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/yeqown/go-qrcode/v2/matrix" +) + +func Test_min(t *testing.T) { + type args struct { + x int + y int + } + tests := []struct { + name string + args args + want int + }{ + { + name: "min", + args: args{ + x: 1, + y: 2, + }, + want: 1, + }, + { + name: "min", + args: args{ + x: 2, + y: 1, + }, + want: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, min(tt.args.x, tt.args.y), "min(%v, %v)", tt.args.x, tt.args.y) + }) + } +} + +func Test_abs(t *testing.T) { + type args struct { + x int + } + tests := []struct { + name string + args args + want int + }{ + { + name: "abs", + args: args{ + x: 1, + }, + want: 1, + }, + { + name: "abs", + args: args{ + x: -1, + }, + want: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, abs(tt.args.x), "abs(%v)", tt.args.x) + }) + } +} + +func Test_samestate(t *testing.T) { + type args struct { + s1 matrix.State + s2 matrix.State + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "case 1", + args: args{ + s1: matrix.StateTrue, + s2: matrix.StateTrue, + }, + want: true, + }, + { + name: "case 2", + args: args{ + s1: matrix.StateFalse, + s2: matrix.StateFalse, + }, + want: true, + }, + { + name: "case 3", + args: args{ + s1: matrix.StateTrue, + s2: matrix.StateFalse, + }, + want: false, + }, + { + name: "case 4", + args: args{ + s1: matrix.StateFalse, + s2: matrix.StateTrue, + }, + want: false, + }, + { + name: "case 5", + args: args{ + s1: matrix.StateFinder, + s2: matrix.StateFinder, + }, + want: true, + }, + { + name: "case 6", + args: args{ + s1: matrix.StateFinder, + s2: matrix.StateFalse, + }, + want: false, + }, + { + name: "case 7", + args: args{ + s1: matrix.StateTrue, + s2: matrix.StateFinder, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, samestate(tt.args.s1, tt.args.s2), "samestate(%v, %v)", tt.args.s1, tt.args.s2) + }) + } +} + +func Benchmark_samestate(b *testing.B) { + for i := 0; i < b.N; i++ { + + samestate(matrix.StateTrue, matrix.StateTrue) + samestate(matrix.StateTrue, matrix.StateVersion) + } +} + +func Test_binaryToStateSlice(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want []matrix.State + }{ + { + name: "case 1", + args: args{ + "1010 0001 101a", + }, + want: []matrix.State{ + // 1010 + matrix.StateTrue, matrix.StateFalse, matrix.StateTrue, matrix.StateFalse, + // 0001 + matrix.StateFalse, matrix.StateFalse, matrix.StateFalse, matrix.StateTrue, + // 101a + matrix.StateTrue, matrix.StateFalse, matrix.StateTrue, + }, + }, + { + name: "case 2", + args: args{ + "0000 11a1 11x2 x", + }, + want: []matrix.State{ + // 0000 + matrix.StateFalse, matrix.StateFalse, matrix.StateFalse, matrix.StateFalse, + // 11a1 + matrix.StateTrue, matrix.StateTrue, matrix.StateTrue, + // 11x2 + matrix.StateTrue, matrix.StateTrue, + // x + // nothing + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, binaryToStateSlice(tt.args.s), "binaryToStateSlice(%v)", tt.args.s) + }) + } +}