From bac8bce7754aa93a9db4719975143eba8a783c57 Mon Sep 17 00:00:00 2001 From: Florian Sundermann Date: Wed, 11 Dec 2013 14:31:11 +0100 Subject: [PATCH] initial commit --- barcode.go | 14 + bitlist.go | 110 ++++++++ ean/eancode.go | 46 ++++ ean/encoder.go | 264 +++++++++++++++++++ qr/alphanumeric.go | 61 +++++ qr/alphanumeric_test.go | 42 +++ qr/automatic.go | 29 +++ qr/automatic_test.go | 29 +++ qr/blocks.go | 59 +++++ qr/blocks_test.go | 22 ++ qr/encoder.go | 508 +++++++++++++++++++++++++++++++++++++ qr/encoder_test.go | 11 + qr/errorcorrection.go | 75 ++++++ qr/errorcorrection_test.go | 50 ++++ qr/numeric.go | 66 +++++ qr/numeric_test.go | 21 ++ qr/qrcode.go | 164 ++++++++++++ qr/qrcode_test.go | 61 +++++ qr/versioninfo.go | 304 ++++++++++++++++++++++ qr/versioninfo_test.go | 143 +++++++++++ scaledbarcode.go | 110 ++++++++ 21 files changed, 2189 insertions(+) create mode 100644 barcode.go create mode 100644 bitlist.go create mode 100644 ean/eancode.go create mode 100644 ean/encoder.go create mode 100644 qr/alphanumeric.go create mode 100644 qr/alphanumeric_test.go create mode 100644 qr/automatic.go create mode 100644 qr/automatic_test.go create mode 100644 qr/blocks.go create mode 100644 qr/blocks_test.go create mode 100644 qr/encoder.go create mode 100644 qr/encoder_test.go create mode 100644 qr/errorcorrection.go create mode 100644 qr/errorcorrection_test.go create mode 100644 qr/numeric.go create mode 100644 qr/numeric_test.go create mode 100644 qr/qrcode.go create mode 100644 qr/qrcode_test.go create mode 100644 qr/versioninfo.go create mode 100644 qr/versioninfo_test.go create mode 100644 scaledbarcode.go diff --git a/barcode.go b/barcode.go new file mode 100644 index 0000000..ebad4cb --- /dev/null +++ b/barcode.go @@ -0,0 +1,14 @@ +package barcode + +import "image" + +type Metadata struct { + CodeKind string + Dimensions byte +} + +type Barcode interface { + image.Image + Metadata() Metadata + Content() string +} diff --git a/bitlist.go b/bitlist.go new file mode 100644 index 0000000..4e93384 --- /dev/null +++ b/bitlist.go @@ -0,0 +1,110 @@ +package barcode + +type BitList struct { + count int + data []int32 +} + +func NewBitList(capacity int) *BitList { + bl := new(BitList) + bl.count = capacity + x := 0 + if capacity%32 != 0 { + x = 1 + } + bl.data = make([]int32, capacity/32+x) + return bl +} + +func (bl *BitList) Len() int { + return bl.count +} + +func (bl *BitList) Cap() int { + return len(bl.data) * 32 +} + +func (bl *BitList) grow() { + growBy := len(bl.data) + if growBy < 128 { + growBy = 128 + } else if growBy >= 1024 { + growBy = 1024 + } + + nd := make([]int32, len(bl.data)+growBy) + copy(nd, bl.data) + bl.data = nd +} + +func (bl *BitList) AddBit(bit bool) { + itmIndex := bl.count / 32 + for itmIndex >= len(bl.data) { + bl.grow() + } + bl.SetBit(bl.count, bit) + bl.count++ +} + +func (bl *BitList) SetBit(index int, value bool) { + itmIndex := index / 32 + itmBitShift := 31 - (index % 32) + if value { + bl.data[itmIndex] = bl.data[itmIndex] | 1<> uint(itmBitShift)) & 1) == 1 +} + +func (bl *BitList) AddByte(b byte) { + for i := 7; i >= 0; i-- { + bl.AddBit(((b >> uint(i)) & 1) == 1) + } +} + +func (bl *BitList) AddBits(b int, count byte) { + for i := int(count - 1); i >= 0; i-- { + bl.AddBit(((b >> uint(i)) & 1) == 1) + } +} + +func (bl *BitList) GetBytes() []byte { + len := bl.count >> 3 + if (bl.count % 8) != 0 { + len += 1 + } + result := make([]byte, len) + for i := 0; i < len; i++ { + shift := (3 - (i % 4)) * 8 + result[i] = (byte)((bl.data[i/4] >> uint(shift)) & 0xFF) + } + return result +} + +func (bl *BitList) ItterateBytes() <-chan byte { + res := make(chan byte) + + go func() { + c := bl.count + shift := 24 + i := 0 + for c > 0 { + res <- byte((bl.data[i] >> uint(shift)) & 0xFF) + shift -= 8 + if shift < 0 { + shift = 24 + i += 1 + } + c -= 8 + } + close(res) + }() + + return res +} diff --git a/ean/eancode.go b/ean/eancode.go new file mode 100644 index 0000000..888a64c --- /dev/null +++ b/ean/eancode.go @@ -0,0 +1,46 @@ +package ean + +import ( + "github.com/boombuler/barcode" + "image" + "image/color" +) + +type eancode struct { + *barcode.BitList + content string +} + +func newEANCode(isEAN8 bool) *eancode { + capacity := 95 + if isEAN8 { + capacity = 67 + } + return &eancode{barcode.NewBitList(capacity), ""} +} + +func (c *eancode) Content() string { + return c.content +} + +func (c *eancode) Metadata() barcode.Metadata { + if c.Len() == 67 { + return barcode.Metadata{"EAN 8", 1} + } + return barcode.Metadata{"EAN 13", 1} +} + +func (c *eancode) ColorModel() color.Model { + return color.Gray16Model +} + +func (c *eancode) Bounds() image.Rectangle { + return image.Rect(0, 0, c.Len(), 1) +} + +func (c *eancode) At(x, y int) color.Color { + if c.GetBit(x) { + return color.Black + } + return color.White +} diff --git a/ean/encoder.go b/ean/encoder.go new file mode 100644 index 0000000..16d5128 --- /dev/null +++ b/ean/encoder.go @@ -0,0 +1,264 @@ +package ean + +import ( + "errors" + "github.com/boombuler/barcode" +) + +type encodedNumber struct { + LeftOdd []bool + LeftEven []bool + Right []bool + CheckSum []bool +} + +var encoderTable map[rune]encodedNumber = map[rune]encodedNumber{ + '0': encodedNumber{ + []bool{false, false, false, true, true, false, true}, + []bool{false, true, false, false, true, true, true}, + []bool{true, true, true, false, false, true, false}, + []bool{false, false, false, false, false, false}, + }, + '1': encodedNumber{ + []bool{false, false, true, true, false, false, true}, + []bool{false, true, true, false, false, true, true}, + []bool{true, true, false, false, true, true, false}, + []bool{false, false, true, false, true, true}, + }, + '2': encodedNumber{ + []bool{false, false, true, false, false, true, true}, + []bool{false, false, true, true, false, true, true}, + []bool{true, true, false, true, true, false, false}, + []bool{false, false, true, true, false, true}, + }, + '3': encodedNumber{ + []bool{false, true, true, true, true, false, true}, + []bool{false, true, false, false, false, false, true}, + []bool{true, false, false, false, false, true, false}, + []bool{false, false, true, true, true, false}, + }, + '4': encodedNumber{ + []bool{false, true, false, false, false, true, true}, + []bool{false, false, true, true, true, false, true}, + []bool{true, false, true, true, true, false, false}, + []bool{false, true, false, false, true, true}, + }, + '5': encodedNumber{ + []bool{false, true, true, false, false, false, true}, + []bool{false, true, true, true, false, false, true}, + []bool{true, false, false, true, true, true, false}, + []bool{false, true, true, false, false, true}, + }, + '6': encodedNumber{ + []bool{false, true, false, true, true, true, true}, + []bool{false, false, false, false, true, false, true}, + []bool{true, false, true, false, false, false, false}, + []bool{false, true, true, true, false, false}, + }, + '7': encodedNumber{ + []bool{false, true, true, true, false, true, true}, + []bool{false, false, true, false, false, false, true}, + []bool{true, false, false, false, true, false, false}, + []bool{false, true, false, true, false, true}, + }, + '8': encodedNumber{ + []bool{false, true, true, false, true, true, true}, + []bool{false, false, false, true, false, false, true}, + []bool{true, false, false, true, false, false, false}, + []bool{false, true, false, true, true, false}, + }, + '9': encodedNumber{ + []bool{false, false, false, true, false, true, true}, + []bool{false, false, true, false, true, true, true}, + []bool{true, true, true, false, true, false, false}, + []bool{false, true, true, false, true, false}, + }, +} + +func runeToInt(r rune) int { + switch r { + case '0': + return 0 + case '1': + return 1 + case '2': + return 2 + case '3': + return 3 + case '4': + return 4 + case '5': + return 5 + case '6': + return 6 + case '7': + return 7 + case '8': + return 8 + case '9': + return 9 + } + return -1 +} + +func intToRune(i int) rune { + switch i { + case 0: + return '0' + case 1: + return '1' + case 2: + return '2' + case 3: + return '3' + case 4: + return '4' + case 5: + return '5' + case 6: + return '6' + case 7: + return '7' + case 8: + return '8' + case 9: + return '9' + } + return 'F' +} + +func calcCheckNum(code string) rune { + x3 := len(code) == 7 + sum := 0 + for _, r := range code { + curNum := runeToInt(r) + if curNum < 0 || curNum > 9 { + return 'B' + } + if x3 { + curNum = curNum * 3 + } + x3 = !x3 + sum += curNum + } + + return intToRune((10 - (sum % 10)) % 10) +} + +func encodeEAN8(code string, result *eancode) bool { + pos := 0 + appendBit := func(b bool) { + result.SetBit(pos, b) + pos++ + } + + appendBit(true) + appendBit(false) + appendBit(true) + + for cpos, r := range code { + num, ok := encoderTable[r] + if !ok { + return false + } + var data []bool + if cpos < 4 { + data = num.LeftOdd + } else { + data = num.Right + } + + if cpos == 4 { + appendBit(false) + appendBit(true) + appendBit(false) + appendBit(true) + appendBit(false) + } + for _, bit := range data { + appendBit(bit) + } + } + + appendBit(true) + appendBit(false) + appendBit(true) + return true +} + +func encodeEAN13(code string, result *eancode) bool { + pos := 0 + appendBit := func(b bool) { + result.SetBit(pos, b) + pos++ + } + + appendBit(true) + appendBit(false) + appendBit(true) + + var firstNum []bool + for cpos, r := range code { + num, ok := encoderTable[r] + if !ok { + return false + } + if cpos == 0 { + firstNum = num.CheckSum + continue + } + + var data []bool + if cpos < 7 { // Left + if firstNum[cpos-1] { + data = num.LeftEven + } else { + data = num.LeftOdd + } + } else { + data = num.Right + } + + if cpos == 7 { + appendBit(false) + appendBit(true) + appendBit(false) + appendBit(true) + appendBit(false) + } + + for _, bit := range data { + appendBit(bit) + } + } + appendBit(true) + appendBit(false) + appendBit(true) + return true +} + +func Encode(code string) (barcode.Barcode, error) { + if len(code) == 7 || len(code) == 12 { + code += string(calcCheckNum(code)) + } else if len(code) == 8 || len(code) == 13 { + check := code[0 : len(code)-1] + check += string(calcCheckNum(check)) + if check != code { + return nil, errors.New("checksum missmatch!") + } + } + ean8 := false + if len(code) == 8 { + ean8 = true + + } else if len(code) != 13 { + return nil, errors.New("invalid ean code data") + } + result := newEANCode(ean8) + if (ean8 && encodeEAN8(code, result)) || (!ean8 && encodeEAN13(code, result)) { + result.content = code + return result, nil + } + + return nil, errors.New("ean code contains invalid characters") +} diff --git a/qr/alphanumeric.go b/qr/alphanumeric.go new file mode 100644 index 0000000..73841c1 --- /dev/null +++ b/qr/alphanumeric.go @@ -0,0 +1,61 @@ +package qr + +import ( + "errors" + "fmt" + "github.com/boombuler/barcode" +) + +type alphaNumericEncoding struct { +} + +var AlphaNumeric Encoding = alphaNumericEncoding{} + +var alphaNumericTable map[byte]int = map[byte]int{ + '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, + 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 'G': 16, 'H': 17, 'I': 18, 'J': 19, + 'K': 20, 'L': 21, 'M': 22, 'N': 23, 'O': 24, 'P': 25, 'Q': 26, 'R': 27, 'S': 28, 'T': 29, + 'U': 30, 'V': 31, 'W': 32, 'X': 33, 'Y': 34, 'Z': 35, ' ': 36, '$': 37, '%': 38, '*': 39, + '+': 40, '-': 41, '.': 42, '/': 43, ':': 44, +} + +func (ane alphaNumericEncoding) String() string { + return "AlphaNumeric" +} + +func (ane alphaNumericEncoding) encode(content string, ecl ErrorCorrectionLevel) (*barcode.BitList, *versionInfo, error) { + + contentLenIsOdd := len(content)%2 == 1 + contentBitCount := (len(content) / 2) * 11 + if contentLenIsOdd { + contentBitCount += 6 + } + vi := findSmallestVersionInfo(ecl, alphaNumericMode, contentBitCount) + if vi == nil { + return nil, nil, errors.New("To much data to encode") + } + + res := new(barcode.BitList) + res.AddBits(int(alphaNumericMode), 4) + res.AddBits(len(content), vi.charCountBits(alphaNumericMode)) + + for idx := 0; idx < len(content)/2; idx++ { + c1, ok1 := alphaNumericTable[content[idx*2]] + c2, ok2 := alphaNumericTable[content[(idx*2)+1]] + if !ok1 || !ok2 { + return nil, nil, fmt.Errorf("\"%s\" can not be encoded as %s", content, ane) + } + res.AddBits(c1*45+c2, 11) + } + if contentLenIsOdd { + c1, ok := alphaNumericTable[content[len(content)-1]] + if !ok { + return nil, nil, fmt.Errorf("\"%s\" can not be encoded as %s", content, ane) + } + res.AddBits(c1, 6) + } + + addPaddingAndTerminator(res, vi) + + return res, vi, nil +} diff --git a/qr/alphanumeric_test.go b/qr/alphanumeric_test.go new file mode 100644 index 0000000..aeb9fdb --- /dev/null +++ b/qr/alphanumeric_test.go @@ -0,0 +1,42 @@ +package qr + +import ( + "bytes" + "testing" +) + +func makeString(length int) string { + res := "" + + for i := 0; i < length; i++ { + res += "A" + } + + return res +} + +func Test_AlphaNumericEncoding(t *testing.T) { + x, vi, err := AlphaNumeric.encode("HELLO WORLD", M) + + if x == nil || vi == nil || vi.Version != 1 || bytes.Compare(x.GetBytes(), []byte{32, 91, 11, 120, 209, 114, 220, 77, 67, 64, 236, 17, 236, 17, 236, 17}) != 0 { + t.Errorf("\"HELLO WORLD\" failed to encode: %s", err) + } + + x, vi, err = AlphaNumeric.encode(makeString(4296), L) + if x == nil || vi == nil || err != nil { + t.Fail() + } + x, vi, err = AlphaNumeric.encode(makeString(4297), L) + if x != nil || vi != nil || err == nil { + t.Fail() + } + x, vi, err = AlphaNumeric.encode("ABc", L) + if x != nil || vi != nil || err == nil { + t.Fail() + } + x, vi, err = AlphaNumeric.encode("hello world", M) + + if x != nil || vi != nil || err == nil { + t.Error("\"hello world\" should not be encodable in alphanumeric mode") + } +} diff --git a/qr/automatic.go b/qr/automatic.go new file mode 100644 index 0000000..fec8817 --- /dev/null +++ b/qr/automatic.go @@ -0,0 +1,29 @@ +package qr + +import ( + "fmt" + "github.com/boombuler/barcode" +) + +type autoEncoding struct { +} + +// choose the best matching encoding +var Auto Encoding = autoEncoding{} + +func (ne autoEncoding) String() string { + return "Auto" +} + +func (ne autoEncoding) encode(content string, ecl ErrorCorrectionLevel) (*barcode.BitList, *versionInfo, error) { + bits, vi, _ := Numeric.encode(content, ecl) + if bits != nil && vi != nil { + return bits, vi, nil + } + bits, vi, _ = AlphaNumeric.encode(content, ecl) + if bits != nil && vi != nil { + return bits, vi, nil + } + + return nil, nil, fmt.Errorf("No encoding found to encode \"%s\"", content) +} diff --git a/qr/automatic_test.go b/qr/automatic_test.go new file mode 100644 index 0000000..9104a03 --- /dev/null +++ b/qr/automatic_test.go @@ -0,0 +1,29 @@ +package qr + +import ( + "bytes" + "testing" +) + +func Test_AutomaticEncoding(t *testing.T) { + tests := map[string]Encoding{ + "0123456789": Numeric, + "ALPHA NUMERIC": AlphaNumeric, + "no matching encoing": nil, + } + + for str, enc := range tests { + testValue, _, _ := Auto.encode(str, M) + if enc != nil { + correctValue, _, _ := enc.encode(str, M) + if testValue == nil || bytes.Compare(correctValue.GetBytes(), testValue.GetBytes()) != 0 { + t.Errorf("wrong encoding used for '%s'", str) + } + } else { + if testValue != nil { + t.Errorf("wrong encoding used for '%s'", str) + } + } + + } +} diff --git a/qr/blocks.go b/qr/blocks.go new file mode 100644 index 0000000..7b909fa --- /dev/null +++ b/qr/blocks.go @@ -0,0 +1,59 @@ +package qr + +type block struct { + data []byte + ecc []byte +} +type blockList []*block + +func splitToBlocks(data <-chan byte, vi *versionInfo) blockList { + result := make(blockList, vi.NumberOfBlocksInGroup1+vi.NumberOfBlocksInGroup2) + + for b := 0; b < int(vi.NumberOfBlocksInGroup1); b++ { + blk := new(block) + blk.data = make([]byte, vi.DataCodeWordsPerBlockInGroup1) + for cw := 0; cw < int(vi.DataCodeWordsPerBlockInGroup1); cw++ { + blk.data[cw] = <-data + } + blk.ecc = gf.calcECC(blk.data, vi.ErrorCorrectionCodewordsPerBlock) + result[b] = blk + } + + for b := 0; b < int(vi.NumberOfBlocksInGroup2); b++ { + blk := new(block) + blk.data = make([]byte, vi.DataCodeWordsPerBlockInGroup2) + for cw := 0; cw < int(vi.DataCodeWordsPerBlockInGroup2); cw++ { + blk.data[cw] = <-data + } + blk.ecc = gf.calcECC(blk.data, vi.ErrorCorrectionCodewordsPerBlock) + result[int(vi.NumberOfBlocksInGroup1)+b] = blk + } + + return result +} + +func (bl blockList) interleave(vi *versionInfo) []byte { + var maxCodewordCount int + if vi.DataCodeWordsPerBlockInGroup1 > vi.DataCodeWordsPerBlockInGroup2 { + maxCodewordCount = int(vi.DataCodeWordsPerBlockInGroup1) + } else { + maxCodewordCount = int(vi.DataCodeWordsPerBlockInGroup2) + } + resultLen := (vi.DataCodeWordsPerBlockInGroup1+vi.ErrorCorrectionCodewordsPerBlock)*vi.NumberOfBlocksInGroup1 + + (vi.DataCodeWordsPerBlockInGroup2+vi.ErrorCorrectionCodewordsPerBlock)*vi.NumberOfBlocksInGroup2 + + result := make([]byte, 0, resultLen) + for i := 0; i < maxCodewordCount; i++ { + for b := 0; b < len(bl); b++ { + if len(bl[b].data) > i { + result = append(result, bl[b].data[i]) + } + } + } + for i := 0; i < int(vi.ErrorCorrectionCodewordsPerBlock); i++ { + for b := 0; b < len(bl); b++ { + result = append(result, bl[b].ecc[i]) + } + } + return result +} diff --git a/qr/blocks_test.go b/qr/blocks_test.go new file mode 100644 index 0000000..99fc2d8 --- /dev/null +++ b/qr/blocks_test.go @@ -0,0 +1,22 @@ +package qr + +import ( + "bytes" + "testing" +) + +func Test_Blocks(t *testing.T) { + byteIt := make(chan byte) + go func() { + for _, b := range []byte{67, 85, 70, 134, 87, 38, 85, 194, 119, 50, 6, 18, 6, 103, 38, 246, 246, 66, 7, 118, 134, 242, 7, 38, 86, 22, 198, 199, 146, 6, 182, 230, 247, 119, 50, 7, 118, 134, 87, 38, 82, 6, 134, 151, 50, 7, 70, 247, 118, 86, 194, 6, 151, 50, 16, 236, 17, 236, 17, 236, 17, 236} { + byteIt <- b + } + close(byteIt) + }() + vi := &versionInfo{5, Q, 18, 2, 15, 2, 16} + + data := splitToBlocks(byteIt, vi).interleave(vi) + if bytes.Compare(data, []byte{67, 246, 182, 70, 85, 246, 230, 247, 70, 66, 247, 118, 134, 7, 119, 86, 87, 118, 50, 194, 38, 134, 7, 6, 85, 242, 118, 151, 194, 7, 134, 50, 119, 38, 87, 16, 50, 86, 38, 236, 6, 22, 82, 17, 18, 198, 6, 236, 6, 199, 134, 17, 103, 146, 151, 236, 38, 6, 50, 17, 7, 236, 213, 87, 148, 235, 199, 204, 116, 159, 11, 96, 177, 5, 45, 60, 212, 173, 115, 202, 76, 24, 247, 182, 133, 147, 241, 124, 75, 59, 223, 157, 242, 33, 229, 200, 238, 106, 248, 134, 76, 40, 154, 27, 195, 255, 117, 129, 230, 172, 154, 209, 189, 82, 111, 17, 10, 2, 86, 163, 108, 131, 161, 163, 240, 32, 111, 120, 192, 178, 39, 133, 141, 236}) != 0 { + t.Fail() + } +} diff --git a/qr/encoder.go b/qr/encoder.go new file mode 100644 index 0000000..6ed548b --- /dev/null +++ b/qr/encoder.go @@ -0,0 +1,508 @@ +package qr + +import ( + "fmt" + "github.com/boombuler/barcode" + "image" +) + +type Encoding interface { + fmt.Stringer + encode(content string, eccLevel ErrorCorrectionLevel) (*barcode.BitList, *versionInfo, error) +} + +func Encode(content string, eccLevel ErrorCorrectionLevel, mode Encoding) (barcode.Barcode, error) { + bits, vi, err := mode.encode(content, eccLevel) + if err != nil { + return nil, err + } + if bits == nil || vi == nil { + return nil, fmt.Errorf("Unable to encode \"%s\" with error correction level %s and encoding mode %s", content, eccLevel, mode) + } + + blocks := splitToBlocks(bits.ItterateBytes(), vi) + data := blocks.interleave(vi) + result := render(data, vi) + result.content = content + return result, nil +} + +func render(data []byte, vi *versionInfo) *qrcode { + dim := vi.modulWidth() + results := make([]*qrcode, 8) + for i := 0; i < 8; i++ { + results[i] = newBarcode(dim) + } + + occupied := newBarcode(dim) + + setAll := func(x int, y int, val bool) { + occupied.Set(x, y, true) + for i := 0; i < 8; i++ { + results[i].Set(x, y, val) + } + } + + drawFinderPatterns(vi, setAll) + drawAlignmentPatterns(occupied, vi, setAll) + + //Timing Pattern: + var i int + for i = 0; i < dim; i++ { + if !occupied.Get(i, 6) { + setAll(i, 6, i%2 == 0) + } + if !occupied.Get(6, i) { + setAll(6, i, i%2 == 0) + } + } + // Dark Module + setAll(8, dim-8, true) + + drawVersionInfo(vi, setAll) + drawFormatInfo(vi, -1, occupied.Set) + for i := 0; i < 8; i++ { + drawFormatInfo(vi, i, results[i].Set) + } + + // Write the data + var curBitNo int = 0 + + for pos := range itterateModules(occupied) { + if curBitNo < len(data)*8 { + curBit := ((data[curBitNo/8] >> uint(7-(curBitNo%8))) & 1) == 1 + for i := 0; i < 8; i++ { + setMasked(pos.X, pos.Y, curBit, i, results[i].Set) + } + curBitNo += 1 + } + } + + lowestPenalty := ^uint(0) + lowestPenaltyIdx := -1 + for i := 0; i < 8; i++ { + p := results[i].calcPenalty() + if p < lowestPenalty { + lowestPenalty = p + lowestPenaltyIdx = i + } + } + return results[lowestPenaltyIdx] +} + +func setMasked(x, y int, val bool, mask int, set func(int, int, bool)) { + switch mask { + case 0: + val = val != (((y + x) % 2) == 0) + break + case 1: + val = val != ((y % 2) == 0) + break + case 2: + val = val != ((x % 3) == 0) + break + case 3: + val = val != (((y + x) % 3) == 0) + break + case 4: + val = val != (((y/2 + x/3) % 2) == 0) + break + case 5: + val = val != (((y*x)%2)+((y*x)%3) == 0) + break + case 6: + val = val != ((((y*x)%2)+((y*x)%3))%2 == 0) + break + case 7: + val = val != ((((y+x)%2)+((y*x)%3))%2 == 0) + } + set(x, y, val) +} + +func itterateModules(occupied *qrcode) <-chan image.Point { + result := make(chan image.Point) + allPoints := make(chan image.Point) + go func() { + curX := occupied.dimension - 1 + curY := occupied.dimension - 1 + isUpward := true + + for true { + if isUpward { + allPoints <- image.Pt(curX, curY) + allPoints <- image.Pt(curX-1, curY) + curY -= 1 + if curY < 0 { + curY = 0 + curX -= 2 + if curX == 6 { + curX -= 1 + } + if curX < 0 { + break + } + isUpward = false + } + } else { + allPoints <- image.Pt(curX, curY) + allPoints <- image.Pt(curX-1, curY) + curY += 1 + if curY >= occupied.dimension { + curY = occupied.dimension - 1 + curX -= 2 + if curX == 6 { + curX -= 1 + } + isUpward = true + if curX < 0 { + break + } + } + } + } + + close(allPoints) + }() + go func() { + for pt := range allPoints { + if !occupied.Get(pt.X, pt.Y) { + result <- pt + } + } + close(result) + }() + return result +} + +func drawFinderPatterns(vi *versionInfo, set func(int, int, bool)) { + dim := vi.modulWidth() + drawPattern := func(xoff int, yoff int) { + for x := -1; x < 8; x++ { + for y := -1; y < 8; y++ { + val := (x == 0 || x == 6 || y == 0 || y == 6 || (x > 1 && x < 5 && y > 1 && y < 5)) && (x <= 6 && y <= 6 && x >= 0 && y >= 0) + + if x+xoff >= 0 && x+xoff < dim && y+yoff >= 0 && y+yoff < dim { + set(x+xoff, y+yoff, val) + } + } + } + } + drawPattern(0, 0) + drawPattern(0, dim-7) + drawPattern(dim-7, 0) +} + +func drawAlignmentPatterns(occupied *qrcode, vi *versionInfo, set func(int, int, bool)) { + drawPattern := func(xoff int, yoff int) { + for x := -2; x <= 2; x++ { + for y := -2; y <= 2; y++ { + val := x == -2 || x == 2 || y == -2 || y == 2 || (x == 0 && y == 0) + set(x+xoff, y+yoff, val) + } + } + } + positions := vi.alignmentPatternPlacements() + + for _, x := range positions { + for _, y := range positions { + if occupied.Get(x, y) { + continue + } + drawPattern(x, y) + } + } +} + +func drawFormatInfo(vi *versionInfo, usedMask int, set func(int, int, bool)) { + var formatInfo []bool + switch vi.Level { + case L: + switch usedMask { + case 0: + formatInfo = []bool{true, true, true, false, true, true, true, true, true, false, false, false, true, false, false} + break + case 1: + formatInfo = []bool{true, true, true, false, false, true, false, true, true, true, true, false, false, true, true} + break + case 2: + formatInfo = []bool{true, true, true, true, true, false, true, true, false, true, false, true, false, true, false} + break + case 3: + formatInfo = []bool{true, true, true, true, false, false, false, true, false, false, true, true, true, false, true} + break + case 4: + formatInfo = []bool{true, true, false, false, true, true, false, false, false, true, false, true, true, true, true} + break + case 5: + formatInfo = []bool{true, true, false, false, false, true, true, false, false, false, true, true, false, false, false} + break + case 6: + formatInfo = []bool{true, true, false, true, true, false, false, false, true, false, false, false, false, false, true} + break + case 7: + formatInfo = []bool{true, true, false, true, false, false, true, false, true, true, true, false, true, true, false} + break + } + break + case M: + switch usedMask { + case 0: + formatInfo = []bool{true, false, true, false, true, false, false, false, false, false, true, false, false, true, false} + break + case 1: + formatInfo = []bool{true, false, true, false, false, false, true, false, false, true, false, false, true, false, true} + break + case 2: + formatInfo = []bool{true, false, true, true, true, true, false, false, true, true, true, true, true, false, false} + break + case 3: + formatInfo = []bool{true, false, true, true, false, true, true, false, true, false, false, true, false, true, true} + break + case 4: + formatInfo = []bool{true, false, false, false, true, false, true, true, true, true, true, true, false, false, true} + break + case 5: + formatInfo = []bool{true, false, false, false, false, false, false, true, true, false, false, true, true, true, false} + break + case 6: + formatInfo = []bool{true, false, false, true, true, true, true, true, false, false, true, false, true, true, true} + break + case 7: + formatInfo = []bool{true, false, false, true, false, true, false, true, false, true, false, false, false, false, false} + break + } + break + case Q: + switch usedMask { + case 0: + formatInfo = []bool{false, true, true, false, true, false, true, false, true, false, true, true, true, true, true} + break + case 1: + formatInfo = []bool{false, true, true, false, false, false, false, false, true, true, false, true, false, false, false} + break + case 2: + formatInfo = []bool{false, true, true, true, true, true, true, false, false, true, true, false, false, false, true} + break + case 3: + formatInfo = []bool{false, true, true, true, false, true, false, false, false, false, false, false, true, true, false} + break + case 4: + formatInfo = []bool{false, true, false, false, true, false, false, true, false, true, true, false, true, false, false} + break + case 5: + formatInfo = []bool{false, true, false, false, false, false, true, true, false, false, false, false, false, true, true} + break + case 6: + formatInfo = []bool{false, true, false, true, true, true, false, true, true, false, true, true, false, true, false} + break + case 7: + formatInfo = []bool{false, true, false, true, false, true, true, true, true, true, false, true, true, false, true} + break + } + break + case H: + switch usedMask { + case 0: + formatInfo = []bool{false, false, true, false, true, true, false, true, false, false, false, true, false, false, true} + break + case 1: + formatInfo = []bool{false, false, true, false, false, true, true, true, false, true, true, true, true, true, false} + break + case 2: + formatInfo = []bool{false, false, true, true, true, false, false, true, true, true, false, false, true, true, true} + break + case 3: + formatInfo = []bool{false, false, true, true, false, false, true, true, true, false, true, false, false, false, false} + break + case 4: + formatInfo = []bool{false, false, false, false, true, true, true, false, true, true, false, false, false, true, false} + break + case 5: + formatInfo = []bool{false, false, false, false, false, true, false, false, true, false, true, false, true, false, true} + break + case 6: + formatInfo = []bool{false, false, false, true, true, false, true, false, false, false, false, true, true, false, false} + break + case 7: + formatInfo = []bool{false, false, false, true, false, false, false, false, false, true, true, true, false, true, true} + break + } + break + } + + if usedMask == -1 { + formatInfo = []bool{true, true, true, true, true, true, true, true, true, true, true, true, true, true, true} // Set all to true cause -1 --> occupied mask. + } + + if len(formatInfo) == 15 { + dim := vi.modulWidth() + set(0, 8, formatInfo[0]) + set(1, 8, formatInfo[1]) + set(2, 8, formatInfo[2]) + set(3, 8, formatInfo[3]) + set(4, 8, formatInfo[4]) + set(5, 8, formatInfo[5]) + set(7, 8, formatInfo[6]) + set(8, 8, formatInfo[7]) + set(8, 7, formatInfo[8]) + set(8, 5, formatInfo[9]) + set(8, 4, formatInfo[10]) + set(8, 3, formatInfo[11]) + set(8, 2, formatInfo[12]) + set(8, 1, formatInfo[13]) + set(8, 0, formatInfo[14]) + + set(8, dim-1, formatInfo[0]) + set(8, dim-2, formatInfo[1]) + set(8, dim-3, formatInfo[2]) + set(8, dim-4, formatInfo[3]) + set(8, dim-5, formatInfo[4]) + set(8, dim-6, formatInfo[5]) + set(8, dim-7, formatInfo[6]) + set(dim-8, 8, formatInfo[7]) + set(dim-7, 8, formatInfo[8]) + set(dim-6, 8, formatInfo[9]) + set(dim-5, 8, formatInfo[10]) + set(dim-4, 8, formatInfo[11]) + set(dim-3, 8, formatInfo[12]) + set(dim-2, 8, formatInfo[13]) + set(dim-1, 8, formatInfo[14]) + } +} + +func drawVersionInfo(vi *versionInfo, set func(int, int, bool)) { + var versionInfoBits []bool + + switch vi.Version { + case 7: + versionInfoBits = []bool{false, false, false, true, true, true, true, true, false, false, true, false, false, true, false, true, false, false} + break + case 8: + versionInfoBits = []bool{false, false, true, false, false, false, false, true, false, true, true, false, true, true, true, true, false, false} + break + case 9: + versionInfoBits = []bool{false, false, true, false, false, true, true, false, true, false, true, false, false, true, true, false, false, true} + break + case 10: + versionInfoBits = []bool{false, false, true, false, true, false, false, true, false, false, true, true, false, true, false, false, true, true} + break + case 11: + versionInfoBits = []bool{false, false, true, false, true, true, true, false, true, true, true, true, true, true, false, true, true, false} + break + case 12: + versionInfoBits = []bool{false, false, true, true, false, false, false, true, true, true, false, true, true, false, false, false, true, false} + break + case 13: + versionInfoBits = []bool{false, false, true, true, false, true, true, false, false, false, false, true, false, false, false, true, true, true} + break + case 14: + versionInfoBits = []bool{false, false, true, true, true, false, false, true, true, false, false, false, false, false, true, true, false, true} + break + case 15: + versionInfoBits = []bool{false, false, true, true, true, true, true, false, false, true, false, false, true, false, true, false, false, false} + break + case 16: + versionInfoBits = []bool{false, true, false, false, false, false, true, false, true, true, false, true, true, true, true, false, false, false} + break + case 17: + versionInfoBits = []bool{false, true, false, false, false, true, false, true, false, false, false, true, false, true, true, true, false, true} + break + case 18: + versionInfoBits = []bool{false, true, false, false, true, false, true, false, true, false, false, false, false, true, false, true, true, true} + break + case 19: + versionInfoBits = []bool{false, true, false, false, true, true, false, true, false, true, false, false, true, true, false, false, true, false} + break + case 20: + versionInfoBits = []bool{false, true, false, true, false, false, true, false, false, true, true, false, true, false, false, true, true, false} + break + case 21: + versionInfoBits = []bool{false, true, false, true, false, true, false, true, true, false, true, false, false, false, false, false, true, true} + break + case 22: + versionInfoBits = []bool{false, true, false, true, true, false, true, false, false, false, true, true, false, false, true, false, false, true} + break + case 23: + versionInfoBits = []bool{false, true, false, true, true, true, false, true, true, true, true, true, true, false, true, true, false, false} + break + case 24: + versionInfoBits = []bool{false, true, true, false, false, false, true, true, true, false, true, true, false, false, false, true, false, false} + break + case 25: + versionInfoBits = []bool{false, true, true, false, false, true, false, false, false, true, true, true, true, false, false, false, false, true} + break + case 26: + versionInfoBits = []bool{false, true, true, false, true, false, true, true, true, true, true, false, true, false, true, false, true, true} + break + case 27: + versionInfoBits = []bool{false, true, true, false, true, true, false, false, false, false, true, false, false, false, true, true, true, false} + break + case 28: + versionInfoBits = []bool{false, true, true, true, false, false, true, true, false, false, false, false, false, true, true, false, true, false} + break + case 29: + versionInfoBits = []bool{false, true, true, true, false, true, false, false, true, true, false, false, true, true, true, true, true, true} + break + case 30: + versionInfoBits = []bool{false, true, true, true, true, false, true, true, false, true, false, true, true, true, false, true, false, true} + break + case 31: + versionInfoBits = []bool{false, true, true, true, true, true, false, false, true, false, false, true, false, true, false, false, false, false} + break + case 32: + versionInfoBits = []bool{true, false, false, false, false, false, true, false, false, true, true, true, false, true, false, true, false, true} + break + case 33: + versionInfoBits = []bool{true, false, false, false, false, true, false, true, true, false, true, true, true, true, false, false, false, false} + break + case 34: + versionInfoBits = []bool{true, false, false, false, true, false, true, false, false, false, true, false, true, true, true, false, true, false} + break + case 35: + versionInfoBits = []bool{true, false, false, false, true, true, false, true, true, true, true, false, false, true, true, true, true, true} + break + case 36: + versionInfoBits = []bool{true, false, false, true, false, false, true, false, true, true, false, false, false, false, true, false, true, true} + break + case 37: + versionInfoBits = []bool{true, false, false, true, false, true, false, true, false, false, false, false, true, false, true, true, true, false} + break + case 38: + versionInfoBits = []bool{true, false, false, true, true, false, true, false, true, false, false, true, true, false, false, true, false, false} + break + case 39: + versionInfoBits = []bool{true, false, false, true, true, true, false, true, false, true, false, true, false, false, false, false, false, true} + break + case 40: + versionInfoBits = []bool{true, false, true, false, false, false, true, true, false, false, false, true, true, false, true, false, false, true} + break + } + + if len(versionInfoBits) > 0 { + for i := 0; i < len(versionInfoBits); i++ { + x := (vi.modulWidth() - 11) + i%3 + y := i / 3 + set(x, y, versionInfoBits[len(versionInfoBits)-i-1]) + set(y, x, versionInfoBits[len(versionInfoBits)-i-1]) + } + } + +} + +func addPaddingAndTerminator(bl *barcode.BitList, vi *versionInfo) { + for i := 0; i < 4 && bl.Len() < vi.totalDataBytes()*8; i++ { + bl.AddBit(false) + } + + for bl.Len()%8 != 0 { + bl.AddBit(false) + } + + for i := 0; bl.Len() < vi.totalDataBytes()*8; i++ { + if i%2 == 0 { + bl.AddByte(236) + } else { + bl.AddByte(17) + } + } +} diff --git a/qr/encoder_test.go b/qr/encoder_test.go new file mode 100644 index 0000000..068c264 --- /dev/null +++ b/qr/encoder_test.go @@ -0,0 +1,11 @@ +package qr + +import "testing" +import "image/png" +import "github.com/boombuler/barcode" +import "os" + +func Test_EncodeQR(t *testing.T) { + + +} diff --git a/qr/errorcorrection.go b/qr/errorcorrection.go new file mode 100644 index 0000000..3a009b1 --- /dev/null +++ b/qr/errorcorrection.go @@ -0,0 +1,75 @@ +package qr + +type galoisField struct { + aLogTbl []byte + logTbl []byte + polynomes map[byte][]byte +} + +var gf *galoisField = newGF() + +func newGF() *galoisField { + result := new(galoisField) + result.polynomes = make(map[byte][]byte) + result.aLogTbl = make([]byte, 255) + result.logTbl = make([]byte, 256) + + result.aLogTbl[0] = 1 + + x := 1 + for i := 1; i < 255; i++ { + x = x * 2 + if x > 255 { + x = x ^ 285 + } + result.aLogTbl[i] = byte(x) + } + + for i := 1; i < 255; i++ { + result.logTbl[result.aLogTbl[i]] = byte(i) + } + + return result +} + +func (gf *galoisField) getPolynom(eccc byte) []byte { + _, ok := gf.polynomes[eccc] + if !ok { + if eccc == 1 { + gf.polynomes[eccc] = []byte{0, 0} + } else { + b1 := gf.getPolynom(eccc - 1) + result := make([]byte, eccc+1) + for x := 0; x < len(b1); x++ { + tmp1 := (int(b1[x]) + int(eccc-1)) % 255 + if x == 0 { + result[x] = b1[x] + } else { + tmp0 := int(gf.aLogTbl[result[x]]) ^ int(gf.aLogTbl[b1[x]]) + result[x] = gf.logTbl[tmp0] + } + result[x+1] = byte(tmp1) + } + gf.polynomes[eccc] = result + + } + } + return gf.polynomes[eccc] +} + +func (gf *galoisField) calcECC(data []byte, eccCount byte) []byte { + tmp := make([]byte, len(data)+int(eccCount)) + copy(tmp, data) + generator := gf.getPolynom(eccCount) + + for i := 0; i < len(data); i++ { + alpha := gf.logTbl[tmp[i]] + for j := 0; j < len(generator); j++ { + idx := (int(alpha) + int(generator[j])) % 255 + polyJ := gf.aLogTbl[idx] + tmp[i+j] = (tmp[i+j] ^ polyJ) + } + } + + return tmp[len(data):] +} diff --git a/qr/errorcorrection_test.go b/qr/errorcorrection_test.go new file mode 100644 index 0000000..a11d58a --- /dev/null +++ b/qr/errorcorrection_test.go @@ -0,0 +1,50 @@ +package qr + +import ( + "bytes" + "testing" +) + +func Test_LogTables(t *testing.T) { + for i := 1; i <= 255; i++ { + tmp := gf.logTbl[i] + if byte(i) != gf.aLogTbl[tmp] { + t.Errorf("Invalid LogTables: %d", i) + } + } + + if gf.aLogTbl[11] != 232 || gf.aLogTbl[87] != 127 || gf.aLogTbl[225] != 36 { + t.Fail() + } +} + +func Test_Polynoms(t *testing.T) { + doTest := func(b []byte) { + cnt := byte(len(b) - 1) + if bytes.Compare(gf.getPolynom(cnt), b) != 0 { + t.Errorf("Failed getPolynom(%d)", cnt) + } + } + doTest([]byte{0, 0}) + doTest([]byte{0, 87, 229, 146, 149, 238, 102, 21}) + doTest([]byte{0, 251, 67, 46, 61, 118, 70, 64, 94, 32, 45}) + doTest([]byte{0, 183, 26, 201, 87, 210, 221, 113, 21, 46, 65, 45, 50, 238, 184, 249, 225, 102, 58, 209, 218, 109, 165, 26, 95, 184, 192, 52, 245, 35, 254, 238, 175, 172, 79, 123, 25, 122, 43, 120, 108, 215, 80, 128, 201, 235, 8, 153, 59, 101, 31, 198, 76, 31, 156}) +} + +func Test_ErrorCorrection(t *testing.T) { + doTest := func(b []byte, ecc []byte) { + cnt := byte(len(ecc)) + if bytes.Compare(gf.calcECC(b, cnt), ecc) != 0 { + t.Fail() + } + } + + data1 := []byte{16, 32, 12, 86, 97, 128, 236, 17, 236, 17, 236, 17, 236, 17, 236, 17} + doTest(data1, []byte{140, 250}) + doTest(data1, []byte{165, 36, 212, 193, 237, 54, 199, 135, 44, 85}) + doTest(data1, []byte{227, 219, 167, 206, 127, 77, 181, 205, 203, 131, 6, 102, 62, 113, 173, 153, 69, 210, 55, 111, 146, 227, 13, 144, 249, 87, 103, 81, 30, 125, 189, 61, 142, 129, 129, 43, 148, 88, 25, 249, 37, 58, 57, 108, 91, 241, 78, 248, 226, 177, 17, 58, 59, 218, 53, 146, 96, 165, 146, 163, 198, 190, 15, 71, 117, 164, 167, 53}) + + data2 := []byte{0, 0, 255, 255} + doTest(data2, []byte{171, 81, 216, 241, 210}) + doTest(data2, []byte{12, 183, 205, 34, 73, 117, 36, 75, 237, 235}) +} diff --git a/qr/numeric.go b/qr/numeric.go new file mode 100644 index 0000000..2c2b078 --- /dev/null +++ b/qr/numeric.go @@ -0,0 +1,66 @@ +package qr + +import ( + "errors" + "fmt" + "github.com/boombuler/barcode" + "strconv" +) + +type numericEncoding struct { +} + +var Numeric Encoding = numericEncoding{} + +func (ne numericEncoding) String() string { + return "Numeric" +} + +func (ne numericEncoding) encode(content string, ecl ErrorCorrectionLevel) (*barcode.BitList, *versionInfo, error) { + contentBitCount := (len(content) / 3) * 10 + switch len(content) % 3 { + case 1: + contentBitCount += 4 + break + case 2: + contentBitCount += 7 + break + } + vi := findSmallestVersionInfo(ecl, numericMode, contentBitCount) + if vi == nil { + return nil, nil, errors.New("To much data to encode") + } + res := new(barcode.BitList) + res.AddBits(int(numericMode), 4) + res.AddBits(len(content), vi.charCountBits(numericMode)) + + for pos := 0; pos < len(content); pos += 3 { + var curStr string + if pos+3 <= len(content) { + curStr = content[pos : pos+3] + } else { + curStr = content[pos:] + } + + i, err := strconv.Atoi(curStr) + if err != nil || i < 0 { + return nil, nil, fmt.Errorf("\"%s\" can not be encoded as %s", content, ne) + } + var bitCnt byte + switch len(curStr) % 3 { + case 0: + bitCnt = 10 + case 1: + bitCnt = 4 + break + case 2: + bitCnt = 7 + break + } + + res.AddBits(i, bitCnt) + } + + addPaddingAndTerminator(res, vi) + return res, vi, nil +} diff --git a/qr/numeric_test.go b/qr/numeric_test.go new file mode 100644 index 0000000..2d8c0b4 --- /dev/null +++ b/qr/numeric_test.go @@ -0,0 +1,21 @@ +package qr + +import ( + "bytes" + "testing" +) + +func Test_NumericEncoding(t *testing.T) { + x, vi, err := Numeric.encode("01234567", H) + if x == nil || vi == nil || vi.Version != 1 || bytes.Compare(x.GetBytes(), []byte{16, 32, 12, 86, 97, 128, 236, 17, 236}) != 0 { + t.Error("\"01234567\" failed to encode") + } + x, vi, err = Numeric.encode("0123456789012345", H) + if x == nil || vi == nil || vi.Version != 1 || bytes.Compare(x.GetBytes(), []byte{16, 64, 12, 86, 106, 110, 20, 234, 80}) != 0 { + t.Error("\"0123456789012345\" failed to encode") + } + x, vi, err = Numeric.encode("foo", H) + if err == nil { + t.Error("Numeric encoding should not be able to encode \"foo\"") + } +} diff --git a/qr/qrcode.go b/qr/qrcode.go new file mode 100644 index 0000000..14f956d --- /dev/null +++ b/qr/qrcode.go @@ -0,0 +1,164 @@ +package qr + +import ( + "github.com/boombuler/barcode" + "image" + "image/color" + "math" +) + +type qrcode struct { + dimension int + data *barcode.BitList + content string +} + +func (qr *qrcode) Content() string { + return qr.content +} + +func (qr *qrcode) Metadata() barcode.Metadata { + return barcode.Metadata{"QR Code", 2} +} + +func (qr *qrcode) ColorModel() color.Model { + return color.Gray16Model +} + +func (qr *qrcode) Bounds() image.Rectangle { + return image.Rect(0, 0, qr.dimension, qr.dimension) +} + +func (qr *qrcode) At(x, y int) color.Color { + if qr.Get(x, y) { + return color.Black + } + return color.White +} + +func (qr *qrcode) Get(x, y int) bool { + return qr.data.GetBit(x*qr.dimension + y) +} + +func (qr *qrcode) Set(x, y int, val bool) { + qr.data.SetBit(x*qr.dimension+y, val) +} + +func (qr *qrcode) calcPenalty() uint { + return qr.calcPenaltyRule1() + qr.calcPenaltyRule2() + qr.calcPenaltyRule3() + qr.calcPenaltyRule4() +} + +func (qr *qrcode) calcPenaltyRule1() uint { + var result uint = 0 + for x := 0; x < qr.dimension; x++ { + checkForX := false + var cntX uint = 0 + checkForY := false + var cntY uint = 0 + + for y := 0; y < qr.dimension; y++ { + if qr.Get(x, y) == checkForX { + cntX += 1 + } else { + checkForX = !checkForX + if cntX >= 5 { + result += cntX - 2 + } + cntX = 0 + } + + if qr.Get(y, x) == checkForY { + cntY += 1 + } else { + checkForY = !checkForY + if cntY >= 5 { + result += cntY - 2 + } + cntY = 0 + } + } + + if cntX >= 5 { + result += cntX - 2 + } + if cntY >= 5 { + result += cntY - 2 + } + } + + return result +} + +func (qr *qrcode) calcPenaltyRule2() uint { + var result uint = 0 + for x := 0; x < qr.dimension-1; x++ { + for y := 0; y < qr.dimension-1; y++ { + check := qr.Get(x, y) + if qr.Get(x, y+1) == check && qr.Get(x+1, y) == check && qr.Get(x+1, y+1) == check { + result += 3 + } + } + } + return result +} + +func (qr *qrcode) calcPenaltyRule3() uint { + var pattern1 []bool = []bool{true, false, true, true, true, false, true, false, false, false, false} + var pattern2 []bool = []bool{false, false, false, false, true, false, true, true, true, false, true} + + var result uint = 0 + for x := 0; x <= qr.dimension-len(pattern1); x++ { + for y := 0; y < qr.dimension; y++ { + pattern1XFound := true + pattern2XFound := true + pattern1YFound := true + pattern2YFound := true + + for i := 0; i < len(pattern1); i++ { + iv := qr.Get(x+i, y) + if iv != pattern1[i] { + pattern1XFound = false + } + if iv != pattern2[i] { + pattern2XFound = false + } + iv = qr.Get(y, x+i) + if iv != pattern1[i] { + pattern1YFound = false + } + if iv != pattern2[i] { + pattern2YFound = false + } + } + if pattern1XFound || pattern2XFound { + result += 40 + } + if pattern1YFound || pattern2YFound { + result += 40 + } + } + } + + return result +} + +func (qr *qrcode) calcPenaltyRule4() uint { + totalNum := qr.data.Len() + trueCnt := 0 + for i := 0; i < totalNum; i++ { + if qr.data.GetBit(i) { + trueCnt += 1 + } + } + percDark := float64(trueCnt) * 100 / float64(totalNum) + floor := math.Abs(math.Floor(percDark/5) - 10) + ceil := math.Abs(math.Ceil(percDark/5) - 10) + return uint(math.Min(floor, ceil) * 10) +} + +func newBarcode(dim int) *qrcode { + res := new(qrcode) + res.dimension = dim + res.data = barcode.NewBitList(dim * dim) + return res +} diff --git a/qr/qrcode_test.go b/qr/qrcode_test.go new file mode 100644 index 0000000..a661622 --- /dev/null +++ b/qr/qrcode_test.go @@ -0,0 +1,61 @@ +package qr + +import ( + "image/color" + "testing" +) + +func Test_NewQRCode(t *testing.T) { + bc := newBarcode(2) + if bc == nil { + t.Fail() + } + if bc.data.Len() != 4 { + t.Fail() + } + if bc.dimension != 2 { + t.Fail() + } +} + +func Test_ImageBasics(t *testing.T) { + qr := newBarcode(10) + if qr.ColorModel() != color.Gray16Model { + t.Fail() + } +} + + + +func Test_Penalty4(t *testing.T) { + qr := newBarcode(3) + if qr.calcPenaltyRule4() != 100 { + t.Fail() + } + qr.Set(0, 0, true) + if qr.calcPenaltyRule4() != 70 { + t.Fail() + } + qr.Set(0, 1, true) + if qr.calcPenaltyRule4() != 50 { + t.Fail() + } + qr.Set(0, 2, true) + if qr.calcPenaltyRule4() != 30 { + t.Fail() + } + qr.Set(1, 0, true) + if qr.calcPenaltyRule4() != 10 { + t.Fail() + } + qr.Set(1, 1, true) + if qr.calcPenaltyRule4() != 10 { + t.Fail() + } + qr = newBarcode(2) + qr.Set(0, 0, true) + qr.Set(1, 0, true) + if qr.calcPenaltyRule4() != 0 { + t.Fail() + } +} diff --git a/qr/versioninfo.go b/qr/versioninfo.go new file mode 100644 index 0000000..7151a25 --- /dev/null +++ b/qr/versioninfo.go @@ -0,0 +1,304 @@ +package qr + +import "math" + +type ErrorCorrectionLevel byte + +const ( + L ErrorCorrectionLevel = iota + M + Q + H +) + +func (ecl ErrorCorrectionLevel) String() string { + switch ecl { + case L: + return "L" + case M: + return "M" + case Q: + return "Q" + case H: + return "H" + } + return "unknown" +} + +type encodingMode byte + +const ( + numericMode encodingMode = 1 + alphaNumericMode encodingMode = 2 + byteMode encodingMode = 4 + kanjiMode encodingMode = 8 +) + +type versionInfo struct { + Version byte + Level ErrorCorrectionLevel + ErrorCorrectionCodewordsPerBlock byte + NumberOfBlocksInGroup1 byte + DataCodeWordsPerBlockInGroup1 byte + NumberOfBlocksInGroup2 byte + DataCodeWordsPerBlockInGroup2 byte +} + +var versionInfos []*versionInfo = []*versionInfo{ + &versionInfo{1, L, 7, 1, 19, 0, 0}, + &versionInfo{1, M, 10, 1, 16, 0, 0}, + &versionInfo{1, Q, 13, 1, 13, 0, 0}, + &versionInfo{1, H, 17, 1, 9, 0, 0}, + &versionInfo{2, L, 10, 1, 34, 0, 0}, + &versionInfo{2, M, 16, 1, 28, 0, 0}, + &versionInfo{2, Q, 22, 1, 22, 0, 0}, + &versionInfo{2, H, 28, 1, 16, 0, 0}, + &versionInfo{3, L, 15, 1, 55, 0, 0}, + &versionInfo{3, M, 26, 1, 44, 0, 0}, + &versionInfo{3, Q, 18, 2, 17, 0, 0}, + &versionInfo{3, H, 22, 2, 13, 0, 0}, + &versionInfo{4, L, 20, 1, 80, 0, 0}, + &versionInfo{4, M, 18, 2, 32, 0, 0}, + &versionInfo{4, Q, 26, 2, 24, 0, 0}, + &versionInfo{4, H, 16, 4, 9, 0, 0}, + &versionInfo{5, L, 26, 1, 108, 0, 0}, + &versionInfo{5, M, 24, 2, 43, 0, 0}, + &versionInfo{5, Q, 18, 2, 15, 2, 16}, + &versionInfo{5, H, 22, 2, 11, 2, 12}, + &versionInfo{6, L, 18, 2, 68, 0, 0}, + &versionInfo{6, M, 16, 4, 27, 0, 0}, + &versionInfo{6, Q, 24, 4, 19, 0, 0}, + &versionInfo{6, H, 28, 4, 15, 0, 0}, + &versionInfo{7, L, 20, 2, 78, 0, 0}, + &versionInfo{7, M, 18, 4, 31, 0, 0}, + &versionInfo{7, Q, 18, 2, 14, 4, 15}, + &versionInfo{7, H, 26, 4, 13, 1, 14}, + &versionInfo{8, L, 24, 2, 97, 0, 0}, + &versionInfo{8, M, 22, 2, 38, 2, 39}, + &versionInfo{8, Q, 22, 4, 18, 2, 19}, + &versionInfo{8, H, 26, 4, 14, 2, 15}, + &versionInfo{9, L, 30, 2, 116, 0, 0}, + &versionInfo{9, M, 22, 3, 36, 2, 37}, + &versionInfo{9, Q, 20, 4, 16, 4, 17}, + &versionInfo{9, H, 24, 4, 12, 4, 13}, + &versionInfo{10, L, 18, 2, 68, 2, 69}, + &versionInfo{10, M, 26, 4, 43, 1, 44}, + &versionInfo{10, Q, 24, 6, 19, 2, 20}, + &versionInfo{10, H, 28, 6, 15, 2, 16}, + &versionInfo{11, L, 20, 4, 81, 0, 0}, + &versionInfo{11, M, 30, 1, 50, 4, 51}, + &versionInfo{11, Q, 28, 4, 22, 4, 23}, + &versionInfo{11, H, 24, 3, 12, 8, 13}, + &versionInfo{12, L, 24, 2, 92, 2, 93}, + &versionInfo{12, M, 22, 6, 36, 2, 37}, + &versionInfo{12, Q, 26, 4, 20, 6, 21}, + &versionInfo{12, H, 28, 7, 14, 4, 15}, + &versionInfo{13, L, 26, 4, 107, 0, 0}, + &versionInfo{13, M, 22, 8, 37, 1, 38}, + &versionInfo{13, Q, 24, 8, 20, 4, 21}, + &versionInfo{13, H, 22, 12, 11, 4, 12}, + &versionInfo{14, L, 30, 3, 115, 1, 116}, + &versionInfo{14, M, 24, 4, 40, 5, 41}, + &versionInfo{14, Q, 20, 11, 16, 5, 17}, + &versionInfo{14, H, 24, 11, 12, 5, 13}, + &versionInfo{15, L, 22, 5, 87, 1, 88}, + &versionInfo{15, M, 24, 5, 41, 5, 42}, + &versionInfo{15, Q, 30, 5, 24, 7, 25}, + &versionInfo{15, H, 24, 11, 12, 7, 13}, + &versionInfo{16, L, 24, 5, 98, 1, 99}, + &versionInfo{16, M, 28, 7, 45, 3, 46}, + &versionInfo{16, Q, 24, 15, 19, 2, 20}, + &versionInfo{16, H, 30, 3, 15, 13, 16}, + &versionInfo{17, L, 28, 1, 107, 5, 108}, + &versionInfo{17, M, 28, 10, 46, 1, 47}, + &versionInfo{17, Q, 28, 1, 22, 15, 23}, + &versionInfo{17, H, 28, 2, 14, 17, 15}, + &versionInfo{18, L, 30, 5, 120, 1, 121}, + &versionInfo{18, M, 26, 9, 43, 4, 44}, + &versionInfo{18, Q, 28, 17, 22, 1, 23}, + &versionInfo{18, H, 28, 2, 14, 19, 15}, + &versionInfo{19, L, 28, 3, 113, 4, 114}, + &versionInfo{19, M, 26, 3, 44, 11, 45}, + &versionInfo{19, Q, 26, 17, 21, 4, 22}, + &versionInfo{19, H, 26, 9, 13, 16, 14}, + &versionInfo{20, L, 28, 3, 107, 5, 108}, + &versionInfo{20, M, 26, 3, 41, 13, 42}, + &versionInfo{20, Q, 30, 15, 24, 5, 25}, + &versionInfo{20, H, 28, 15, 15, 10, 16}, + &versionInfo{21, L, 28, 4, 116, 4, 117}, + &versionInfo{21, M, 26, 17, 42, 0, 0}, + &versionInfo{21, Q, 28, 17, 22, 6, 23}, + &versionInfo{21, H, 30, 19, 16, 6, 17}, + &versionInfo{22, L, 28, 2, 111, 7, 112}, + &versionInfo{22, M, 28, 17, 46, 0, 0}, + &versionInfo{22, Q, 30, 7, 24, 16, 25}, + &versionInfo{22, H, 24, 34, 13, 0, 0}, + &versionInfo{23, L, 30, 4, 121, 5, 122}, + &versionInfo{23, M, 28, 4, 47, 14, 48}, + &versionInfo{23, Q, 30, 11, 24, 14, 25}, + &versionInfo{23, H, 30, 16, 15, 14, 16}, + &versionInfo{24, L, 30, 6, 117, 4, 118}, + &versionInfo{24, M, 28, 6, 45, 14, 46}, + &versionInfo{24, Q, 30, 11, 24, 16, 25}, + &versionInfo{24, H, 30, 30, 16, 2, 17}, + &versionInfo{25, L, 26, 8, 106, 4, 107}, + &versionInfo{25, M, 28, 8, 47, 13, 48}, + &versionInfo{25, Q, 30, 7, 24, 22, 25}, + &versionInfo{25, H, 30, 22, 15, 13, 16}, + &versionInfo{26, L, 28, 10, 114, 2, 115}, + &versionInfo{26, M, 28, 19, 46, 4, 47}, + &versionInfo{26, Q, 28, 28, 22, 6, 23}, + &versionInfo{26, H, 30, 33, 16, 4, 17}, + &versionInfo{27, L, 30, 8, 122, 4, 123}, + &versionInfo{27, M, 28, 22, 45, 3, 46}, + &versionInfo{27, Q, 30, 8, 23, 26, 24}, + &versionInfo{27, H, 30, 12, 15, 28, 16}, + &versionInfo{28, L, 30, 3, 117, 10, 118}, + &versionInfo{28, M, 28, 3, 45, 23, 46}, + &versionInfo{28, Q, 30, 4, 24, 31, 25}, + &versionInfo{28, H, 30, 11, 15, 31, 16}, + &versionInfo{29, L, 30, 7, 116, 7, 117}, + &versionInfo{29, M, 28, 21, 45, 7, 46}, + &versionInfo{29, Q, 30, 1, 23, 37, 24}, + &versionInfo{29, H, 30, 19, 15, 26, 16}, + &versionInfo{30, L, 30, 5, 115, 10, 116}, + &versionInfo{30, M, 28, 19, 47, 10, 48}, + &versionInfo{30, Q, 30, 15, 24, 25, 25}, + &versionInfo{30, H, 30, 23, 15, 25, 16}, + &versionInfo{31, L, 30, 13, 115, 3, 116}, + &versionInfo{31, M, 28, 2, 46, 29, 47}, + &versionInfo{31, Q, 30, 42, 24, 1, 25}, + &versionInfo{31, H, 30, 23, 15, 28, 16}, + &versionInfo{32, L, 30, 17, 115, 0, 0}, + &versionInfo{32, M, 28, 10, 46, 23, 47}, + &versionInfo{32, Q, 30, 10, 24, 35, 25}, + &versionInfo{32, H, 30, 19, 15, 35, 16}, + &versionInfo{33, L, 30, 17, 115, 1, 116}, + &versionInfo{33, M, 28, 14, 46, 21, 47}, + &versionInfo{33, Q, 30, 29, 24, 19, 25}, + &versionInfo{33, H, 30, 11, 15, 46, 16}, + &versionInfo{34, L, 30, 13, 115, 6, 116}, + &versionInfo{34, M, 28, 14, 46, 23, 47}, + &versionInfo{34, Q, 30, 44, 24, 7, 25}, + &versionInfo{34, H, 30, 59, 16, 1, 17}, + &versionInfo{35, L, 30, 12, 121, 7, 122}, + &versionInfo{35, M, 28, 12, 47, 26, 48}, + &versionInfo{35, Q, 30, 39, 24, 14, 25}, + &versionInfo{35, H, 30, 22, 15, 41, 16}, + &versionInfo{36, L, 30, 6, 121, 14, 122}, + &versionInfo{36, M, 28, 6, 47, 34, 48}, + &versionInfo{36, Q, 30, 46, 24, 10, 25}, + &versionInfo{36, H, 30, 2, 15, 64, 16}, + &versionInfo{37, L, 30, 17, 122, 4, 123}, + &versionInfo{37, M, 28, 29, 46, 14, 47}, + &versionInfo{37, Q, 30, 49, 24, 10, 25}, + &versionInfo{37, H, 30, 24, 15, 46, 16}, + &versionInfo{38, L, 30, 4, 122, 18, 123}, + &versionInfo{38, M, 28, 13, 46, 32, 47}, + &versionInfo{38, Q, 30, 48, 24, 14, 25}, + &versionInfo{38, H, 30, 42, 15, 32, 16}, + &versionInfo{39, L, 30, 20, 117, 4, 118}, + &versionInfo{39, M, 28, 40, 47, 7, 48}, + &versionInfo{39, Q, 30, 43, 24, 22, 25}, + &versionInfo{39, H, 30, 10, 15, 67, 16}, + &versionInfo{40, L, 30, 19, 118, 6, 119}, + &versionInfo{40, M, 28, 18, 47, 31, 48}, + &versionInfo{40, Q, 30, 34, 24, 34, 25}, + &versionInfo{40, H, 30, 20, 15, 61, 16}, +} + +func (vi *versionInfo) totalDataBytes() int { + g1Data := int(vi.NumberOfBlocksInGroup1) * int(vi.DataCodeWordsPerBlockInGroup1) + g2Data := int(vi.NumberOfBlocksInGroup2) * int(vi.DataCodeWordsPerBlockInGroup2) + return (g1Data + g2Data) +} + +func (vi *versionInfo) charCountBits(m encodingMode) byte { + if m == numericMode { + if vi.Version < 10 { + return 10 + } else if vi.Version < 27 { + return 12 + } else { + return 14 + } + } else if m == alphaNumericMode { + if vi.Version < 10 { + return 9 + } else if vi.Version < 27 { + return 11 + } else { + return 13 + } + } else if m == byteMode { + if vi.Version < 10 { + return 8 + } else { + return 16 + } + } else if m == kanjiMode { + if vi.Version < 10 { + return 8 + } else if vi.Version < 27 { + return 10 + } else { + return 12 + } + } + return 0 +} + +func (vi *versionInfo) modulWidth() int { + return ((int(vi.Version) - 1) * 4) + 21 +} + +func (vi *versionInfo) alignmentPatternPlacements() []int { + if vi.Version == 1 { + return make([]int, 0) + } + + var first int = 6 + var last int = vi.modulWidth() - 7 + var space float64 = float64(last - first) + count := int(math.Ceil(space/28)) + 1 + + result := make([]int, count) + result[0] = first + result[len(result)-1] = last + if count > 2 { + step := int(math.Ceil(float64(last-first) / float64(count-1))) + if step%2 == 1 { + frac := float64(last-first) / float64(count-1) + _, x := math.Modf(frac) + if x >= 0.5 { + frac = math.Ceil(frac) + } else { + frac = math.Floor(frac) + } + + if int(frac)%2 == 0 { + step -= 1 + } else { + step += 1 + } + } + + for i := 1; i <= count-2; i++ { + result[i] = last - (step * (count - 1 - i)) + } + } + + return result +} + +func findSmallestVersionInfo(ecl ErrorCorrectionLevel, mode encodingMode, dataBits int) *versionInfo { + dataBits = dataBits + 4 // mode indicator + for _, vi := range versionInfos { + if vi.Level == ecl { + if (vi.totalDataBytes() * 8) >= (dataBits + int(vi.charCountBits(mode))) { + return vi + } + } + } + return nil +} diff --git a/qr/versioninfo_test.go b/qr/versioninfo_test.go new file mode 100644 index 0000000..ec0237d --- /dev/null +++ b/qr/versioninfo_test.go @@ -0,0 +1,143 @@ +package qr + +import "testing" + +var testvi *versionInfo = &versionInfo{7, M, 0, 1, 10, 2, 5} // Fake versionInfo to run some of the tests + +func Test_CharCountBits(t *testing.T) { + v1 := &versionInfo{5, M, 0, 0, 0, 0, 0} + v2 := &versionInfo{15, M, 0, 0, 0, 0, 0} + v3 := &versionInfo{30, M, 0, 0, 0, 0, 0} + + if v1.charCountBits(numericMode) != 10 { + t.Fail() + } + if v1.charCountBits(alphaNumericMode) != 9 { + t.Fail() + } + if v1.charCountBits(byteMode) != 8 { + t.Fail() + } + if v1.charCountBits(kanjiMode) != 8 { + t.Fail() + } + if v2.charCountBits(numericMode) != 12 { + t.Fail() + } + if v2.charCountBits(alphaNumericMode) != 11 { + t.Fail() + } + if v2.charCountBits(byteMode) != 16 { + t.Fail() + } + if v2.charCountBits(kanjiMode) != 10 { + t.Fail() + } + if v3.charCountBits(numericMode) != 14 { + t.Fail() + } + if v3.charCountBits(alphaNumericMode) != 13 { + t.Fail() + } + if v3.charCountBits(byteMode) != 16 { + t.Fail() + } + if v3.charCountBits(kanjiMode) != 12 { + t.Fail() + } +} + +func Test_TotalDataBytes(t *testing.T) { + if testvi.totalDataBytes() != 20 { + t.Fail() + } +} + +func Test_ModulWidth(t *testing.T) { + if testvi.modulWidth() != 45 { + t.Fail() + } +} + +func Test_FindSmallestVersionInfo(t *testing.T) { + if findSmallestVersionInfo(H, alphaNumericMode, 10208) != nil { + t.Error("there should be no version with this capacity") + } + test := func(cap int, tVersion byte) { + v := findSmallestVersionInfo(H, alphaNumericMode, cap) + if v == nil || v.Version != tVersion { + t.Errorf("version %d should be returned.", tVersion) + } + } + test(10191, 40) + test(5591, 29) + test(5592, 30) + test(190, 3) + test(200, 4) +} + +type aligmnentTest struct { + version byte + patterns []int +} + +var allAligmnentTests []*aligmnentTest = []*aligmnentTest{ + &aligmnentTest{1, []int{}}, + &aligmnentTest{2, []int{6, 18}}, + &aligmnentTest{3, []int{6, 22}}, + &aligmnentTest{4, []int{6, 26}}, + &aligmnentTest{5, []int{6, 30}}, + &aligmnentTest{6, []int{6, 34}}, + &aligmnentTest{7, []int{6, 22, 38}}, + &aligmnentTest{8, []int{6, 24, 42}}, + &aligmnentTest{9, []int{6, 26, 46}}, + &aligmnentTest{10, []int{6, 28, 50}}, + &aligmnentTest{11, []int{6, 30, 54}}, + &aligmnentTest{12, []int{6, 32, 58}}, + &aligmnentTest{13, []int{6, 34, 62}}, + &aligmnentTest{14, []int{6, 26, 46, 66}}, + &aligmnentTest{15, []int{6, 26, 48, 70}}, + &aligmnentTest{16, []int{6, 26, 50, 74}}, + &aligmnentTest{17, []int{6, 30, 54, 78}}, + &aligmnentTest{18, []int{6, 30, 56, 82}}, + &aligmnentTest{19, []int{6, 30, 58, 86}}, + &aligmnentTest{20, []int{6, 34, 62, 90}}, + &aligmnentTest{21, []int{6, 28, 50, 72, 94}}, + &aligmnentTest{22, []int{6, 26, 50, 74, 98}}, + &aligmnentTest{23, []int{6, 30, 54, 78, 102}}, + &aligmnentTest{24, []int{6, 28, 54, 80, 106}}, + &aligmnentTest{25, []int{6, 32, 58, 84, 110}}, + &aligmnentTest{26, []int{6, 30, 58, 86, 114}}, + &aligmnentTest{27, []int{6, 34, 62, 90, 118}}, + &aligmnentTest{28, []int{6, 26, 50, 74, 98, 122}}, + &aligmnentTest{29, []int{6, 30, 54, 78, 102, 126}}, + &aligmnentTest{30, []int{6, 26, 52, 78, 104, 130}}, + &aligmnentTest{31, []int{6, 30, 56, 82, 108, 134}}, + &aligmnentTest{32, []int{6, 34, 60, 86, 112, 138}}, + &aligmnentTest{33, []int{6, 30, 58, 86, 114, 142}}, + &aligmnentTest{34, []int{6, 34, 62, 90, 118, 146}}, + &aligmnentTest{35, []int{6, 30, 54, 78, 102, 126, 150}}, + &aligmnentTest{36, []int{6, 24, 50, 76, 102, 128, 154}}, + &aligmnentTest{37, []int{6, 28, 54, 80, 106, 132, 158}}, + &aligmnentTest{38, []int{6, 32, 58, 84, 110, 136, 162}}, + &aligmnentTest{39, []int{6, 26, 54, 82, 110, 138, 166}}, + &aligmnentTest{40, []int{6, 30, 58, 86, 114, 142, 170}}, +} + +func Test_AlignmentPatternPlacements(t *testing.T) { + for _, at := range allAligmnentTests { + vi := &versionInfo{at.version, M, 0, 0, 0, 0, 0} + + res := vi.alignmentPatternPlacements() + if len(res) != len(at.patterns) { + t.Errorf("number of alignmentpatterns missmatch for version %d", at.version) + } + for i := 0; i < len(res); i++ { + if res[i] != at.patterns[i] { + t.Errorf("alignmentpatterns for version %d missmatch on index %d", at.version, i) + } + } + + } + +} diff --git a/scaledbarcode.go b/scaledbarcode.go new file mode 100644 index 0000000..afbaa8f --- /dev/null +++ b/scaledbarcode.go @@ -0,0 +1,110 @@ +package barcode + +import ( + "errors" + "fmt" + "image" + "image/color" + "math" +) + +type wrapFunc func(x, y int) color.Color + +type scaledBarcode struct { + wrapped Barcode + wrapperFunc wrapFunc + rect image.Rectangle +} + +func (bc *scaledBarcode) Content() string { + return bc.wrapped.Content() +} + +func (bc *scaledBarcode) Metadata() Metadata { + return bc.wrapped.Metadata() +} + +func (bc *scaledBarcode) ColorModel() color.Model { + return bc.wrapped.ColorModel() +} + +func (bc *scaledBarcode) Bounds() image.Rectangle { + return bc.rect +} + +func (bc *scaledBarcode) At(x, y int) color.Color { + return bc.wrapperFunc(x, y) +} + +func Scale(bc Barcode, width, height int) (Barcode, error) { + switch bc.Metadata().Dimensions { + case 1: + return scale1DCode(bc, width, height) + case 2: + return scale2DCode(bc, width, height) + } + + return nil, errors.New("unsupported barcode format") +} + +func scale2DCode(bc Barcode, width, height int) (Barcode, error) { + orgBounds := bc.Bounds() + orgWidth := orgBounds.Max.X - orgBounds.Min.X + orgHeight := orgBounds.Max.Y - orgBounds.Min.Y + + factor := int(math.Min(float64(width)/float64(orgWidth), float64(height)/float64(orgHeight))) + if factor <= 0 { + return nil, fmt.Errorf("can no scale barcode to an image smaller then %dx%d", orgWidth, orgHeight) + } + + offsetX := (width - (orgWidth * factor)) / 2 + offsetY := (height - (orgHeight * factor)) / 2 + + wrap := func(x, y int) color.Color { + if x < offsetX || y < offsetY { + return color.White + } + x = (x - offsetX) / factor + y = (y - offsetY) / factor + if x >= orgWidth || y >= orgHeight { + return color.White + } + return bc.At(x, y) + } + + return &scaledBarcode{ + bc, + wrap, + image.Rect(0, 0, width, height), + }, nil +} + +func scale1DCode(bc Barcode, width, height int) (Barcode, error) { + orgBounds := bc.Bounds() + orgWidth := orgBounds.Max.X - orgBounds.Min.X + factor := int(float64(width) / float64(orgWidth)) + + if factor <= 0 { + return nil, fmt.Errorf("can no scale barcode to an image smaller then %dx1", orgWidth) + } + offsetX := (width - (orgWidth * factor)) / 2 + + wrap := func(x, y int) color.Color { + if x < offsetX { + return color.White + } + x = (x - offsetX) / factor + + if x >= orgWidth { + return color.White + } + return bc.At(x, 0) + } + + return &scaledBarcode{ + bc, + wrap, + image.Rect(0, 0, width, height), + }, nil + +}