739 lines
17 KiB
Go
739 lines
17 KiB
Go
|
package qrcode
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"log"
|
||
|
"math"
|
||
|
"sync"
|
||
|
|
||
|
"github.com/yeqown/reedsolomon"
|
||
|
"github.com/yeqown/reedsolomon/binary"
|
||
|
)
|
||
|
|
||
|
// New generate a QRCode struct to create
|
||
|
func New(text string) (*QRCode, error) {
|
||
|
dst := DefaultEncodingOption()
|
||
|
return build(text, dst)
|
||
|
}
|
||
|
|
||
|
// NewWith generate a QRCode struct with
|
||
|
// specified `ver`(QR version) and `ecLv`(Error Correction level)
|
||
|
func NewWith(text string, opts ...EncodeOption) (*QRCode, error) {
|
||
|
dst := DefaultEncodingOption()
|
||
|
for _, opt := range opts {
|
||
|
opt.apply(dst)
|
||
|
}
|
||
|
|
||
|
return build(text, dst)
|
||
|
}
|
||
|
|
||
|
func build(text string, option *encodingOption) (*QRCode, error) {
|
||
|
qrc := &QRCode{
|
||
|
sourceText: text,
|
||
|
sourceRawBytes: []byte(text),
|
||
|
dataBSet: nil,
|
||
|
mat: nil,
|
||
|
ecBSet: nil,
|
||
|
v: version{},
|
||
|
encodingOption: option,
|
||
|
encoder: nil,
|
||
|
}
|
||
|
// initialize QRCode instance
|
||
|
if err := qrc.init(); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
qrc.masking()
|
||
|
|
||
|
return qrc, nil
|
||
|
}
|
||
|
|
||
|
// QRCode contains fields to generate QRCode matrix, outputImageOptions to Draw image,
|
||
|
// etc.
|
||
|
type QRCode struct {
|
||
|
sourceText string // sourceText input text
|
||
|
sourceRawBytes []byte // raw Data to transfer
|
||
|
|
||
|
dataBSet *binary.Binary // final data bit stream of encode data
|
||
|
mat *Matrix // matrix grid to store final bitmap
|
||
|
ecBSet *binary.Binary // final error correction bitset
|
||
|
|
||
|
encodingOption *encodingOption
|
||
|
encoder *encoder // encoder ptr to call its methods ~
|
||
|
v version // indicate the QR version to encode.
|
||
|
}
|
||
|
|
||
|
func (q *QRCode) Save(w Writer) error {
|
||
|
if w == nil {
|
||
|
w = nonWriter{}
|
||
|
}
|
||
|
|
||
|
defer func() {
|
||
|
if err := w.Close(); err != nil {
|
||
|
log.Printf("[WARNNING] [go-qrcode] close writer failed: %v\n", err)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
return w.Write(*q.mat)
|
||
|
}
|
||
|
|
||
|
func (q *QRCode) Dimension() int {
|
||
|
if q.mat == nil {
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
return q.mat.Width()
|
||
|
}
|
||
|
|
||
|
// init fill QRCode instance from settings and sourceText.
|
||
|
func (q *QRCode) init() (err error) {
|
||
|
// choose encode mode (num, alpha num, byte, Japanese)
|
||
|
if q.encodingOption.EncMode == EncModeAuto {
|
||
|
q.encodingOption.EncMode = analyzeEncodeModeFromRaw(q.sourceRawBytes)
|
||
|
}
|
||
|
|
||
|
// choose version
|
||
|
if _, err = q.calcVersion(); err != nil {
|
||
|
return fmt.Errorf("init: calc version failed: %v", err)
|
||
|
}
|
||
|
q.mat = newMatrix(q.v.Dimension(), q.v.Dimension())
|
||
|
_ = q.applyEncoder()
|
||
|
|
||
|
var (
|
||
|
dataBlocks []dataBlock // data encoding blocks
|
||
|
ecBlocks []ecBlock // error correction blocks
|
||
|
)
|
||
|
|
||
|
// data encoding, and be split into blocks
|
||
|
if dataBlocks, err = q.dataEncoding(); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// generate er bitsets, and also be split into blocks
|
||
|
if ecBlocks, err = q.errorCorrectionEncoding(dataBlocks); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// arrange data blocks and EC blocks
|
||
|
q.arrangeBits(dataBlocks, ecBlocks)
|
||
|
// append ec bits after data bits
|
||
|
q.dataBSet.Append(q.ecBSet)
|
||
|
// append remainder bits
|
||
|
q.dataBSet.AppendNumBools(q.v.RemainderBits, false)
|
||
|
// initial the 2d matrix
|
||
|
q.prefillMatrix()
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// calcVersion
|
||
|
func (q *QRCode) calcVersion() (ver *version, err error) {
|
||
|
var needAnalyze = true
|
||
|
|
||
|
opt := q.encodingOption
|
||
|
if opt.Version >= 1 && opt.Version <= 40 &&
|
||
|
opt.EcLevel >= ErrorCorrectionLow && opt.EcLevel <= ErrorCorrectionHighest {
|
||
|
// only version and EC level are specified, can skip analyzeVersionAuto
|
||
|
needAnalyze = false
|
||
|
}
|
||
|
|
||
|
// automatically parse version
|
||
|
if needAnalyze {
|
||
|
// analyzeVersion the input data to choose to adapt version
|
||
|
analyzed, err2 := analyzeVersion(q.sourceRawBytes, opt.EcLevel, opt.EncMode)
|
||
|
if err2 != nil {
|
||
|
err = fmt.Errorf("calcVersion: analyzeVersionAuto failed: %v", err2)
|
||
|
return nil, err
|
||
|
}
|
||
|
opt.Version = analyzed.Ver
|
||
|
}
|
||
|
|
||
|
q.v = loadVersion(opt.Version, opt.EcLevel)
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// applyEncoder
|
||
|
func (q *QRCode) applyEncoder() error {
|
||
|
q.encoder = newEncoder(q.encodingOption.EncMode, q.encodingOption.EcLevel, q.v)
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// dataEncoding ref to:
|
||
|
// https://www.thonky.com/qr-code-tutorial/data-encoding
|
||
|
func (q *QRCode) dataEncoding() (blocks []dataBlock, err error) {
|
||
|
var (
|
||
|
bset *binary.Binary
|
||
|
)
|
||
|
bset, err = q.encoder.Encode(q.sourceRawBytes)
|
||
|
if err != nil {
|
||
|
err = fmt.Errorf("could not encode data: %v", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
blocks = make([]dataBlock, q.v.TotalNumBlocks())
|
||
|
|
||
|
// split bitset into data Block
|
||
|
start, end, blockID := 0, 0, 0
|
||
|
for _, g := range q.v.Groups {
|
||
|
for j := 0; j < g.NumBlocks; j++ {
|
||
|
start = end
|
||
|
end = start + g.NumDataCodewords*8
|
||
|
|
||
|
blocks[blockID].Data, err = bset.Subset(start, end)
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
blocks[blockID].StartOffset = end - start
|
||
|
blocks[blockID].NumECBlock = g.ECBlockwordsPerBlock
|
||
|
|
||
|
blockID++
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// dataBlock ...
|
||
|
type dataBlock struct {
|
||
|
Data *binary.Binary
|
||
|
StartOffset int // length
|
||
|
NumECBlock int // error correction codewords num per data block
|
||
|
}
|
||
|
|
||
|
// ecBlock ...
|
||
|
type ecBlock struct {
|
||
|
Data *binary.Binary
|
||
|
// StartOffset int // length
|
||
|
}
|
||
|
|
||
|
// errorCorrectionEncoding ref to:
|
||
|
// https://www.thonky.com/qr-code-tutorial /error-correction-coding
|
||
|
func (q *QRCode) errorCorrectionEncoding(dataBlocks []dataBlock) (blocks []ecBlock, err error) {
|
||
|
// start, end, blockID := 0, 0, 0
|
||
|
blocks = make([]ecBlock, q.v.TotalNumBlocks())
|
||
|
for idx, b := range dataBlocks {
|
||
|
debugLogf("numOfECBlock: %d", b.NumECBlock)
|
||
|
bset := reedsolomon.Encode(b.Data, b.NumECBlock)
|
||
|
blocks[idx].Data, err = bset.Subset(b.StartOffset, bset.Len())
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
// blocks[idx].StartOffset = b.StartOffset
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// arrangeBits ... and save into dataBSet
|
||
|
func (q *QRCode) arrangeBits(dataBlocks []dataBlock, ecBlocks []ecBlock) {
|
||
|
if debugEnabled() {
|
||
|
log.Println("arrangeBits called, before")
|
||
|
for i := 0; i < len(ecBlocks); i++ {
|
||
|
debugLogf("ec block_%d: %v", i, ecBlocks[i])
|
||
|
}
|
||
|
for i := 0; i < len(dataBlocks); i++ {
|
||
|
debugLogf("data block_%d: %v", i, dataBlocks[i])
|
||
|
}
|
||
|
}
|
||
|
// arrange data blocks
|
||
|
var (
|
||
|
overflowCnt = 0
|
||
|
endFlag = false
|
||
|
curIdx = 0
|
||
|
start, end int
|
||
|
)
|
||
|
|
||
|
// check if bitsets initialized, or initial them
|
||
|
if q.dataBSet == nil {
|
||
|
q.dataBSet = binary.New()
|
||
|
}
|
||
|
if q.ecBSet == nil {
|
||
|
q.ecBSet = binary.New()
|
||
|
}
|
||
|
|
||
|
for !endFlag {
|
||
|
for _, block := range dataBlocks {
|
||
|
start = curIdx * 8
|
||
|
end = start + 8
|
||
|
if start >= block.Data.Len() {
|
||
|
overflowCnt++
|
||
|
continue
|
||
|
}
|
||
|
subBin, err := block.Data.Subset(start, end)
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
q.dataBSet.Append(subBin)
|
||
|
debugLogf("arrange data blocks info: start: %d, end: %d, len: %d, overflowCnt: %d, curIdx: %d",
|
||
|
start, end, block.Data.Len(), overflowCnt, curIdx,
|
||
|
)
|
||
|
}
|
||
|
curIdx++
|
||
|
// loop finish check
|
||
|
if overflowCnt >= len(dataBlocks) {
|
||
|
endFlag = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// arrange ec blocks and reinitialize
|
||
|
endFlag = false
|
||
|
overflowCnt = 0
|
||
|
curIdx = 0
|
||
|
|
||
|
for !endFlag {
|
||
|
for _, block := range ecBlocks {
|
||
|
start = curIdx * 8
|
||
|
end = start + 8
|
||
|
|
||
|
if start >= block.Data.Len() {
|
||
|
overflowCnt++
|
||
|
continue
|
||
|
}
|
||
|
subBin, err := block.Data.Subset(start, end)
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
q.ecBSet.Append(subBin)
|
||
|
}
|
||
|
curIdx++
|
||
|
// loop finish check
|
||
|
if overflowCnt >= len(ecBlocks) {
|
||
|
endFlag = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
debugLogf("arrangeBits called, after")
|
||
|
debugLogf("data bitsets: %s", q.dataBSet.String())
|
||
|
debugLogf("ec bitsets: %s", q.ecBSet.String())
|
||
|
}
|
||
|
|
||
|
// prefillMatrix with version info: ref to:
|
||
|
// http://www.thonky.com/qr-code-tutorial/module-placement-matrix
|
||
|
func (q *QRCode) prefillMatrix() {
|
||
|
dimension := q.v.Dimension()
|
||
|
if q.mat == nil {
|
||
|
q.mat = newMatrix(dimension, dimension)
|
||
|
}
|
||
|
|
||
|
// add finder left-top
|
||
|
addFinder(q.mat, 0, 0)
|
||
|
addSplitter(q.mat, 7, 7, dimension)
|
||
|
debugLogf("finish left-top finder")
|
||
|
// add finder right-top
|
||
|
addFinder(q.mat, dimension-7, 0)
|
||
|
addSplitter(q.mat, dimension-8, 7, dimension)
|
||
|
debugLogf("finish right-top finder")
|
||
|
// add finder left-bottom
|
||
|
addFinder(q.mat, 0, dimension-7)
|
||
|
addSplitter(q.mat, 7, dimension-8, dimension)
|
||
|
debugLogf("finish left-bottom finder")
|
||
|
|
||
|
// only version-1 QR code has no alignment module
|
||
|
if q.v.Ver > 1 {
|
||
|
// add align-mode related to version cfg
|
||
|
for _, loc := range loadAlignmentPatternLoc(q.v.Ver) {
|
||
|
addAlignment(q.mat, loc.X, loc.Y)
|
||
|
}
|
||
|
debugLogf("finish align")
|
||
|
}
|
||
|
// add timing line
|
||
|
addTimingLine(q.mat, dimension)
|
||
|
// add darkBlock always be position (4*ver+9, 8)
|
||
|
addDarkBlock(q.mat, 8, 4*q.v.Ver+9)
|
||
|
// reserveFormatBlock for version and format info
|
||
|
reserveFormatBlock(q.mat, dimension)
|
||
|
|
||
|
// reserveVersionBlock for version over 7
|
||
|
// only version 7 and larger version should add version info
|
||
|
if q.v.Ver >= 7 {
|
||
|
reserveVersionBlock(q.mat, dimension)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// add finder module
|
||
|
func addFinder(m *Matrix, top, left int) {
|
||
|
// black outer
|
||
|
x, y := top, left
|
||
|
for i := 0; i < 24; i++ {
|
||
|
_ = m.set(x, y, QRValue_FINDER_V1)
|
||
|
if i < 6 {
|
||
|
x = x + 1
|
||
|
} else if i < 12 {
|
||
|
y = y + 1
|
||
|
} else if i < 18 {
|
||
|
x = x - 1
|
||
|
} else {
|
||
|
y = y - 1
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// white inner
|
||
|
x, y = top+1, left+1
|
||
|
for i := 0; i < 16; i++ {
|
||
|
_ = m.set(x, y, QRValue_FINDER_V0)
|
||
|
if i < 4 {
|
||
|
x = x + 1
|
||
|
} else if i < 8 {
|
||
|
y = y + 1
|
||
|
} else if i < 12 {
|
||
|
x = x - 1
|
||
|
} else {
|
||
|
y = y - 1
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// black inner
|
||
|
for x = left + 2; x < left+5; x++ {
|
||
|
for y = top + 2; y < top+5; y++ {
|
||
|
_ = m.set(x, y, QRValue_FINDER_V1)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// add splitter module
|
||
|
func addSplitter(m *Matrix, x, y, dimension int) {
|
||
|
// top-left
|
||
|
if x == 7 && y == 7 {
|
||
|
for pos := 0; pos < 8; pos++ {
|
||
|
_ = m.set(x, pos, QRValue_SPLITTER_V0)
|
||
|
_ = m.set(pos, y, QRValue_SPLITTER_V0)
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// top-right
|
||
|
if x == dimension-8 && y == 7 {
|
||
|
for pos := 0; pos < 8; pos++ {
|
||
|
_ = m.set(x, y-pos, QRValue_SPLITTER_V0)
|
||
|
_ = m.set(x+pos, y, QRValue_SPLITTER_V0)
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// bottom-left
|
||
|
if x == 7 && y == dimension-8 {
|
||
|
for pos := 0; pos < 8; pos++ {
|
||
|
_ = m.set(x, y+pos, QRValue_SPLITTER_V0)
|
||
|
_ = m.set(x-pos, y, QRValue_SPLITTER_V0)
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
// add matrix align module
|
||
|
func addAlignment(m *Matrix, centerX, centerY int) {
|
||
|
_ = m.set(centerX, centerY, QRValue_DATA_V1)
|
||
|
// black
|
||
|
x, y := centerX-2, centerY-2
|
||
|
for i := 0; i < 16; i++ {
|
||
|
_ = m.set(x, y, QRValue_DATA_V1)
|
||
|
if i < 4 {
|
||
|
x = x + 1
|
||
|
} else if i < 8 {
|
||
|
y = y + 1
|
||
|
} else if i < 12 {
|
||
|
x = x - 1
|
||
|
} else {
|
||
|
y = y - 1
|
||
|
}
|
||
|
}
|
||
|
// white
|
||
|
x, y = centerX-1, centerY-1
|
||
|
for i := 0; i < 8; i++ {
|
||
|
_ = m.set(x, y, QRValue_DATA_V0)
|
||
|
if i < 2 {
|
||
|
x = x + 1
|
||
|
} else if i < 4 {
|
||
|
y = y + 1
|
||
|
} else if i < 6 {
|
||
|
x = x - 1
|
||
|
} else {
|
||
|
y = y - 1
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// addTimingLine ...
|
||
|
func addTimingLine(m *Matrix, dimension int) {
|
||
|
for pos := 8; pos < dimension-8; pos++ {
|
||
|
if pos%2 == 0 {
|
||
|
_ = m.set(6, pos, QRValue_TIMING_V1)
|
||
|
_ = m.set(pos, 6, QRValue_TIMING_V1)
|
||
|
} else {
|
||
|
_ = m.set(6, pos, QRValue_TIMING_V0)
|
||
|
_ = m.set(pos, 6, QRValue_TIMING_V0)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// addDarkBlock ...
|
||
|
func addDarkBlock(m *Matrix, x, y int) {
|
||
|
_ = m.set(x, y, QRValue_DARK_V1)
|
||
|
}
|
||
|
|
||
|
// reserveFormatBlock maintain the position in matrix for format info
|
||
|
func reserveFormatBlock(m *Matrix, dimension int) {
|
||
|
for pos := 1; pos < 9; pos++ {
|
||
|
// skip timing line
|
||
|
if pos == 6 {
|
||
|
_ = m.set(8, dimension-pos, QRValue_FORMAT_V0)
|
||
|
_ = m.set(dimension-pos, 8, QRValue_FORMAT_V0)
|
||
|
continue
|
||
|
}
|
||
|
// skip dark module
|
||
|
if pos == 8 {
|
||
|
_ = m.set(8, pos, QRValue_FORMAT_V0) // top-left-column
|
||
|
_ = m.set(pos, 8, QRValue_FORMAT_V0) // top-left-row
|
||
|
_ = m.set(dimension-pos, 8, QRValue_FORMAT_V0) // top-right-row
|
||
|
continue
|
||
|
}
|
||
|
_ = m.set(8, pos, QRValue_FORMAT_V0) // top-left-column
|
||
|
_ = m.set(pos, 8, QRValue_FORMAT_V0) // top-left-row
|
||
|
_ = m.set(dimension-pos, 8, QRValue_FORMAT_V0) // top-right-row
|
||
|
_ = m.set(8, dimension-pos, QRValue_FORMAT_V0) // bottom-left-column
|
||
|
}
|
||
|
|
||
|
// fix(@yeqown): b4b5ae3 reduced two format reversed blocks on top-left-column and top-left-row.
|
||
|
_ = m.set(0, 8, QRValue_FORMAT_V0)
|
||
|
_ = m.set(8, 0, QRValue_FORMAT_V0)
|
||
|
}
|
||
|
|
||
|
// reserveVersionBlock maintain the position in matrix for version info
|
||
|
func reserveVersionBlock(m *Matrix, dimension int) {
|
||
|
// 3x6=18 cells
|
||
|
for i := 1; i <= 3; i++ {
|
||
|
for pos := 0; pos < 6; pos++ {
|
||
|
_ = m.set(dimension-8-i, pos, QRValue_VERSION_V0)
|
||
|
_ = m.set(pos, dimension-8-i, QRValue_VERSION_V0)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// fillDataBinary fill q.dataBSet binary stream into q.mat.
|
||
|
// References:
|
||
|
// * http://www.thonky.com/qr-code-tutorial/module-placement-matrix#Place-the-Data-Bits
|
||
|
//
|
||
|
func (q *QRCode) fillDataBinary(m *Matrix, dimension int) {
|
||
|
var (
|
||
|
// x always move from right, left right loop (2 rows), y move upward, downward, upward loop
|
||
|
x, y = dimension - 1, dimension - 1
|
||
|
l = q.dataBSet.Len()
|
||
|
upForward = true
|
||
|
pos int
|
||
|
)
|
||
|
|
||
|
for i := 0; pos < l; i++ {
|
||
|
// debugLogf("fillDataBinary: dimension: %d, len: %d: pos: %d", dimension, l, pos)
|
||
|
set := QRValue_DATA_V0
|
||
|
if q.dataBSet.At(pos) {
|
||
|
set = QRValue_DATA_V1
|
||
|
}
|
||
|
|
||
|
state, err := m.at(x, y)
|
||
|
if err != nil {
|
||
|
if err == ErrorOutRangeOfW {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
if err == ErrorOutRangeOfH {
|
||
|
// turn around while y is out of range.
|
||
|
x = x - 2
|
||
|
switch upForward {
|
||
|
case true:
|
||
|
y = y + 1
|
||
|
default:
|
||
|
y = y - 1
|
||
|
}
|
||
|
|
||
|
if x == 7 || x == 6 {
|
||
|
x = x - 1
|
||
|
}
|
||
|
upForward = !upForward
|
||
|
state, err = m.at(x, y) // renew state qrbool after turn around writing direction.
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// data bit should only be set into un-set block in matrix.
|
||
|
if state.qrtype() == QRType_INIT {
|
||
|
_ = m.set(x, y, set)
|
||
|
pos++
|
||
|
debugLogf("normal set turn forward: upForward: %v, x: %d, y: %d", upForward, x, y)
|
||
|
}
|
||
|
|
||
|
// DO NOT CHANGE FOLLOWING CODE FOR NOW !!!
|
||
|
// change x, y
|
||
|
mod2 := i % 2
|
||
|
|
||
|
// in one 8bit block
|
||
|
if upForward {
|
||
|
if mod2 == 0 {
|
||
|
x = x - 1
|
||
|
} else {
|
||
|
y = y - 1
|
||
|
x = x + 1
|
||
|
}
|
||
|
} else {
|
||
|
if mod2 == 0 {
|
||
|
x = x - 1
|
||
|
} else {
|
||
|
y = y + 1
|
||
|
x = x + 1
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
debugLogf("fillDone and x: %d, y: %d, pos: %d, total: %d", x, y, pos, l)
|
||
|
}
|
||
|
|
||
|
// draw from bitset to matrix.Matrix, calculate all mask modula score,
|
||
|
// then decide which mask to use according to the mask's score (the lowest one).
|
||
|
func (q *QRCode) masking() {
|
||
|
type maskScore struct {
|
||
|
Score int
|
||
|
Idx int
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
masks = make([]*mask, 8)
|
||
|
mats = make([]*Matrix, 8)
|
||
|
lowScore = math.MaxInt32
|
||
|
markMatsIdx int
|
||
|
scoreChan = make(chan maskScore, 8)
|
||
|
wg sync.WaitGroup
|
||
|
)
|
||
|
|
||
|
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] = cpy.Copy()
|
||
|
}
|
||
|
|
||
|
// generate 8 matrix with mask
|
||
|
for i := 0; i < 8; i++ {
|
||
|
wg.Add(1)
|
||
|
go func(i int) {
|
||
|
_ = debugDraw(fmt.Sprintf("draft/mats_%d.jpeg", i), *mats[i])
|
||
|
_ = debugDraw(fmt.Sprintf("draft/mask_%d.jpeg", i), *masks[i].mat)
|
||
|
|
||
|
// xor with mask
|
||
|
q.xorMask(mats[i], masks[i])
|
||
|
|
||
|
_ = debugDraw(fmt.Sprintf("draft/mats_mask_%d.jpeg", i), *mats[i])
|
||
|
|
||
|
// fill format info
|
||
|
q.fillFormatInfo(mats[i], maskPatternModulo(i), dimension)
|
||
|
// version7 and larger version has version info
|
||
|
if q.v.Ver >= 7 {
|
||
|
q.fillVersionInfo(mats[i], dimension)
|
||
|
}
|
||
|
|
||
|
// calculate score and decide the lowest score and Draw
|
||
|
score := evaluation(mats[i])
|
||
|
debugLogf("cur idx: %d, score: %d, current lowest: mats[%d]:%d", i, score, markMatsIdx, lowScore)
|
||
|
scoreChan <- maskScore{
|
||
|
Score: score,
|
||
|
Idx: i,
|
||
|
}
|
||
|
|
||
|
_ = debugDraw(fmt.Sprintf("draft/qrcode_mask_%d.jpeg", i), *mats[i])
|
||
|
|
||
|
wg.Done()
|
||
|
}(i)
|
||
|
}
|
||
|
|
||
|
wg.Wait()
|
||
|
close(scoreChan)
|
||
|
|
||
|
for c := range scoreChan {
|
||
|
if c.Score < lowScore {
|
||
|
lowScore = c.Score
|
||
|
markMatsIdx = c.Idx
|
||
|
}
|
||
|
}
|
||
|
|
||
|
q.mat = mats[markMatsIdx]
|
||
|
}
|
||
|
|
||
|
// all mask patter and check the maskScore choose the lowest mask result
|
||
|
func (q *QRCode) xorMask(m *Matrix, mask *mask) {
|
||
|
mask.mat.iter(IterDirection_COLUMN, func(x, y int, v qrvalue) {
|
||
|
// skip the empty place
|
||
|
if v.qrtype() == QRType_INIT {
|
||
|
return
|
||
|
}
|
||
|
v2, _ := m.at(x, y)
|
||
|
_ = m.set(x, y, v2.xor(v))
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// fillVersionInfo ref to:
|
||
|
// https://www.thonky.com/qr-code-tutorial/format-version-tables
|
||
|
func (q *QRCode) fillVersionInfo(m *Matrix, dimension int) {
|
||
|
bin := q.v.verInfo()
|
||
|
|
||
|
// from high bit to lowest
|
||
|
pos := 0
|
||
|
for j := 5; j >= 0; j-- {
|
||
|
for i := 1; i <= 3; i++ {
|
||
|
if bin.At(pos) {
|
||
|
_ = m.set(dimension-8-i, j, QRValue_VERSION_V1)
|
||
|
_ = m.set(j, dimension-8-i, QRValue_VERSION_V1)
|
||
|
} else {
|
||
|
_ = m.set(dimension-8-i, j, QRValue_VERSION_V0)
|
||
|
_ = m.set(j, dimension-8-i, QRValue_VERSION_V0)
|
||
|
}
|
||
|
|
||
|
pos++
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// fill format info ref to:
|
||
|
// https://www.thonky.com/qr-code-tutorial/format-version-tables
|
||
|
func (q *QRCode) fillFormatInfo(m *Matrix, mode maskPatternModulo, dimension int) {
|
||
|
fmtBSet := q.v.formatInfo(int(mode))
|
||
|
debugLogf("fmtBitSet: %s", fmtBSet.String())
|
||
|
var (
|
||
|
x, y = 0, dimension - 1
|
||
|
)
|
||
|
|
||
|
for pos := 0; pos < 15; pos++ {
|
||
|
if fmtBSet.At(pos) {
|
||
|
// row
|
||
|
_ = m.set(x, 8, QRValue_FORMAT_V1)
|
||
|
// column
|
||
|
_ = m.set(8, y, QRValue_FORMAT_V1)
|
||
|
} else {
|
||
|
// row
|
||
|
_ = m.set(x, 8, QRValue_FORMAT_V0)
|
||
|
// column
|
||
|
_ = m.set(8, y, QRValue_FORMAT_V0)
|
||
|
}
|
||
|
|
||
|
x = x + 1
|
||
|
y = y - 1
|
||
|
|
||
|
// row skip
|
||
|
if x == 6 {
|
||
|
x = 7
|
||
|
} else if x == 8 {
|
||
|
x = dimension - 8
|
||
|
}
|
||
|
|
||
|
// column skip
|
||
|
if y == dimension-8 {
|
||
|
y = 8
|
||
|
} else if y == 6 {
|
||
|
y = 5
|
||
|
}
|
||
|
}
|
||
|
}
|