finished aztec code generator.

fixes #10
This commit is contained in:
boombuler committed 2016-12-26 22:16:28 +01:00
1 parent 3dbe04fe04
commit d039b6bd02
14 files changed
+642 -160

No files matched your search

+94
View File
@@ -0,0 +1,94 @@
package aztec
import (
"testing"
)
func encodeTest(t *testing.T, data, wanted string) {
result, err := Encode([]byte(data), DEFAULT_EC_PERCENT, DEFAULT_LAYERS)
if err != nil {
t.Error(err)
} else {
ac, ok := result.(*aztecCode)
if !ok {
t.Error("returned barcode is no aztec code...")
} else if draw := ac.string(); draw != wanted {
t.Errorf("Invalid Barcode returned:\n%s", draw)
}
}
}
func Test_Encode1(t *testing.T) {
encodeTest(t, "This is an example Aztec symbol for Wikipedia.",
"X X X X X X X X \n"+
"X X X X X X X X X X \n"+
"X X X X X X X X X X X \n"+
"X X X X X X X X X X X \n"+
" X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X \n"+
"X X X X X X X X X X \n"+
" X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X \n"+
" X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X \n"+
" X X X \n"+
" X X X X X X X X X X \n"+
" X X X X X X X X X X \n")
}
func Test_Encode2(t *testing.T) {
encodeTest(t, "Aztec Code is a public domain 2D matrix barcode symbology"+
" of nominally square symbols built on a square grid with a "+
"distinctive square bullseye pattern at their center.",
" X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X X X X X X X X X \n"+
" X X X X X X X X X X X X X X X X \n"+
"X X X X X X X X X X X X X \n")
}
+66
View File
@@ -0,0 +1,66 @@
package aztec
import (
"bytes"
"image"
"image/color"
"github.com/boombuler/barcode"
"github.com/boombuler/barcode/utils"
)
type aztecCode struct {
*utils.BitList
size int
content []byte
}
func newAztecCode(size int) *aztecCode {
return &aztecCode{utils.NewBitList(size * size), size, nil}
}
func (c *aztecCode) Content() string {
return string(c.content)
}
func (c *aztecCode) Metadata() barcode.Metadata {
return barcode.Metadata{"Aztec", 2}
}
func (c *aztecCode) ColorModel() color.Model {
return color.Gray16Model
}
func (c *aztecCode) Bounds() image.Rectangle {
return image.Rect(0, 0, c.size, c.size)
}
func (c *aztecCode) At(x, y int) color.Color {
if c.GetBit(x*c.size + y) {
return color.Black
}
return color.White
}
func (c *aztecCode) CheckSum() int {
return 0
}
func (c *aztecCode) set(x, y int) {
c.SetBit(x*c.size+y, true)
}
func (c *aztecCode) string() string {
buf := new(bytes.Buffer)
for y := 0; y < c.size; y++ {
for x := 0; x < c.size; x++ {
if c.GetBit(x*c.size + y) {
buf.WriteString("X ")
} else {
buf.WriteString(" ")
}
}
buf.WriteRune('\n')
}
return buf.String()
}
+267
View File
@@ -0,0 +1,267 @@
package aztec
import (
"fmt"
"github.com/boombuler/barcode"
"github.com/boombuler/barcode/utils"
)
const (
DEFAULT_EC_PERCENT = 33
DEFAULT_LAYERS = 0
max_nb_bits = 32
max_nb_bits_compact = 4
)
var (
word_size = []int{
4, 6, 6, 8, 8, 8, 8, 8, 8, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
}
)
func totalBitsInLayer(layers int, compact bool) int {
tmp := 112
if compact {
tmp = 88
}
return (tmp + 16*layers) * layers
}
func stuffBits(bits *utils.BitList, wordSize int) *utils.BitList {
out := new(utils.BitList)
n := bits.Len()
mask := (1 << uint(wordSize)) - 2
for i := 0; i < n; i += wordSize {
word := 0
for j := 0; j < wordSize; j++ {
if i+j >= n || bits.GetBit(i+j) {
word |= 1 << uint(wordSize-1-j)
}
}
if (word & mask) == mask {
out.AddBits(word&mask, byte(wordSize))
i--
} else if (word & mask) == 0 {
out.AddBits(word|1, byte(wordSize))
i--
} else {
out.AddBits(word, byte(wordSize))
}
}
return out
}
func generateModeMessage(compact bool, layers, messageSizeInWords int) *utils.BitList {
modeMessage := new(utils.BitList)
if compact {
modeMessage.AddBits(layers-1, 2)
modeMessage.AddBits(messageSizeInWords-1, 6)
modeMessage = generateCheckWords(modeMessage, 28, 4)
} else {
modeMessage.AddBits(layers-1, 5)
modeMessage.AddBits(messageSizeInWords-1, 11)
modeMessage = generateCheckWords(modeMessage, 40, 4)
}
return modeMessage
}
func drawModeMessage(matrix *aztecCode, compact bool, matrixSize int, modeMessage *utils.BitList) {
center := matrixSize / 2
if compact {
for i := 0; i < 7; i++ {
offset := center - 3 + i
if modeMessage.GetBit(i) {
matrix.set(offset, center-5)
}
if modeMessage.GetBit(i + 7) {
matrix.set(center+5, offset)
}
if modeMessage.GetBit(20 - i) {
matrix.set(offset, center+5)
}
if modeMessage.GetBit(27 - i) {
matrix.set(center-5, offset)
}
}
} else {
for i := 0; i < 10; i++ {
offset := center - 5 + i + i/5
if modeMessage.GetBit(i) {
matrix.set(offset, center-7)
}
if modeMessage.GetBit(i + 10) {
matrix.set(center+7, offset)
}
if modeMessage.GetBit(29 - i) {
matrix.set(offset, center+7)
}
if modeMessage.GetBit(39 - i) {
matrix.set(center-7, offset)
}
}
}
}
func drawBullsEye(matrix *aztecCode, center, size int) {
for i := 0; i < size; i += 2 {
for j := center - i; j <= center+i; j++ {
matrix.set(j, center-i)
matrix.set(j, center+i)
matrix.set(center-i, j)
matrix.set(center+i, j)
}
}
matrix.set(center-size, center-size)
matrix.set(center-size+1, center-size)
matrix.set(center-size, center-size+1)
matrix.set(center+size, center-size)
matrix.set(center+size, center-size+1)
matrix.set(center+size, center+size-1)
}
// Encode returns an aztec barcode with the given content
func Encode(data []byte, minECCPercent int, userSpecifiedLayers int) (barcode.Barcode, error) {
bits := highlevelEncode(data)
eccBits := ((bits.Len() * minECCPercent) / 100) + 11
totalSizeBits := bits.Len() + eccBits
var layers, TotalBitsInLayer, wordSize int
var compact bool
var stuffedBits *utils.BitList
if userSpecifiedLayers != DEFAULT_LAYERS {
compact = userSpecifiedLayers < 0
if compact {
layers = -userSpecifiedLayers
} else {
layers = userSpecifiedLayers
}
if (compact && layers > max_nb_bits_compact) || (!compact && layers > max_nb_bits) {
return nil, fmt.Errorf("Illegal value %d for layers", userSpecifiedLayers)
}
TotalBitsInLayer = totalBitsInLayer(layers, compact)
wordSize = word_size[layers]
usableBitsInLayers := TotalBitsInLayer - (TotalBitsInLayer % wordSize)
stuffedBits = stuffBits(bits, wordSize)
if stuffedBits.Len()+eccBits > usableBitsInLayers {
return nil, fmt.Errorf("Data to large for user specified layer")
}
if compact && stuffedBits.Len() > wordSize*64 {
return nil, fmt.Errorf("Data to large for user specified layer")
}
} else {
wordSize = 0
stuffedBits = nil
// We look at the possible table sizes in the order Compact1, Compact2, Compact3,
// Compact4, Normal4,... Normal(i) for i < 4 isn't typically used since Compact(i+1)
// is the same size, but has more data.
for i := 0; ; i++ {
if i > max_nb_bits {
return nil, fmt.Errorf("Data too large for an aztec code")
}
compact = i <= 3
layers = i
if compact {
layers = i + 1
}
TotalBitsInLayer = totalBitsInLayer(layers, compact)
if totalSizeBits > TotalBitsInLayer {
continue
}
// [Re]stuff the bits if this is the first opportunity, or if the
// wordSize has changed
if wordSize != word_size[layers] {
wordSize = word_size[layers]
stuffedBits = stuffBits(bits, wordSize)
}
usableBitsInLayers := TotalBitsInLayer - (TotalBitsInLayer % wordSize)
if compact && stuffedBits.Len() > wordSize*64 {
// Compact format only allows 64 data words, though C4 can hold more words than that
continue
}
if stuffedBits.Len()+eccBits <= usableBitsInLayers {
break
}
}
}
messageBits := generateCheckWords(stuffedBits, TotalBitsInLayer, wordSize)
messageSizeInWords := stuffedBits.Len() / wordSize
modeMessage := generateModeMessage(compact, layers, messageSizeInWords)
// allocate symbol
var baseMatrixSize int
if compact {
baseMatrixSize = 11 + layers*4
} else {
baseMatrixSize = 14 + layers*4
}
alignmentMap := make([]int, baseMatrixSize)
var matrixSize int
if compact {
// no alignment marks in compact mode, alignmentMap is a no-op
matrixSize = baseMatrixSize
for i := 0; i < len(alignmentMap); i++ {
alignmentMap[i] = i
}
} else {
matrixSize = baseMatrixSize + 1 + 2*((baseMatrixSize/2-1)/15)
origCenter := baseMatrixSize / 2
center := matrixSize / 2
for i := 0; i < origCenter; i++ {
newOffset := i + i/15
alignmentMap[origCenter-i-1] = center - newOffset - 1
alignmentMap[origCenter+i] = center + newOffset + 1
}
}
code := newAztecCode(matrixSize)
code.content = data
// draw data bits
for i, rowOffset := 0, 0; i < layers; i++ {
rowSize := (layers - i) * 4
if compact {
rowSize += 9
} else {
rowSize += 12
}
for j := 0; j < rowSize; j++ {
columnOffset := j * 2
for k := 0; k < 2; k++ {
if messageBits.GetBit(rowOffset + columnOffset + k) {
code.set(alignmentMap[i*2+k], alignmentMap[i*2+j])
}
if messageBits.GetBit(rowOffset + rowSize*2 + columnOffset + k) {
code.set(alignmentMap[i*2+j], alignmentMap[baseMatrixSize-1-i*2-k])
}
if messageBits.GetBit(rowOffset + rowSize*4 + columnOffset + k) {
code.set(alignmentMap[baseMatrixSize-1-i*2-k], alignmentMap[baseMatrixSize-1-i*2-j])
}
if messageBits.GetBit(rowOffset + rowSize*6 + columnOffset + k) {
code.set(alignmentMap[baseMatrixSize-1-i*2-j], alignmentMap[i*2+k])
}
}
}
rowOffset += rowSize * 8
}
// draw mode message
drawModeMessage(code, compact, matrixSize, modeMessage)
// draw alignment marks
if compact {
drawBullsEye(code, matrixSize/2, 5)
} else {
drawBullsEye(code, matrixSize/2, 7)
for i, j := 0, 0; i < baseMatrixSize/2-1; i, j = i+15, j+16 {
for k := (matrixSize / 2) & 1; k < matrixSize; k += 2 {
code.set(matrixSize/2-j, k)
code.set(matrixSize/2+j, k)
code.set(k, matrixSize/2-j)
code.set(k, matrixSize/2+j)
}
}
}
return code, nil
}
+58
View File
@@ -0,0 +1,58 @@
package aztec
import (
"strings"
"testing"
"github.com/boombuler/barcode/utils"
)
func Test_StuffBits(t *testing.T) {
testStuffBits := func(wordSize int, bits string, expected string) {
bl := new(utils.BitList)
for _, r := range bits {
if r == 'X' {
bl.AddBit(true)
} else if r == '.' {
bl.AddBit(false)
}
}
stuffed := stuffBits(bl, wordSize)
expectedBits := strings.Replace(expected, " ", "", -1)
result := bitStr(stuffed)
if result != expectedBits {
t.Errorf("stuffBits failed for %q\nGot: %q", bits, result)
}
}
testStuffBits(5, ".X.X. X.X.X .X.X.",
".X.X. X.X.X .X.X.")
testStuffBits(5, ".X.X. ..... .X.X",
".X.X. ....X ..X.X")
testStuffBits(3, "XX. ... ... ..X XXX .X. ..",
"XX. ..X ..X ..X ..X .XX XX. .X. ..X")
testStuffBits(6, ".X.X.. ...... ..X.XX",
".X.X.. .....X. ..X.XX XXXX.")
testStuffBits(6, ".X.X.. ...... ...... ..X.X.",
".X.X.. .....X .....X ....X. X.XXXX")
testStuffBits(6, ".X.X.. XXXXXX ...... ..X.XX",
".X.X.. XXXXX. X..... ...X.X XXXXX.")
testStuffBits(6,
"...... ..XXXX X..XX. .X.... .X.X.X .....X .X.... ...X.X .....X ....XX ..X... ....X. X..XXX X.XX.X",
".....X ...XXX XX..XX ..X... ..X.X. X..... X.X... ....X. X..... X....X X..X.. .....X X.X..X XXX.XX .XXXXX")
}
func Test_ModeMessage(t *testing.T) {
testModeMessage := func(compact bool, layers, words int, expected string) {
result := bitStr(generateModeMessage(compact, layers, words))
expectedBits := strings.Replace(expected, " ", "", -1)
if result != expectedBits {
t.Errorf("generateModeMessage(%v, %d, %d) failed.\nGot:%s", compact, layers, words, result)
}
}
testModeMessage(true, 2, 29, ".X .XXX.. ...X XX.. ..X .XX. .XX.X")
testModeMessage(true, 4, 64, "XX XXXXXX .X.. ...X ..XX .X.. XX..")
testModeMessage(false, 21, 660, "X.X.. .X.X..X..XX .XXX ..X.. .XXX. .X... ..XXX")
testModeMessage(false, 32, 4096, "XXXXX XXXXXXXXXXX X.X. ..... XXX.X ..X.. X.XXX")
}
+61
View File
@@ -0,0 +1,61 @@
package aztec
import (
"github.com/boombuler/barcode/utils"
)
func bitsToWords(stuffedBits *utils.BitList, wordSize int, wordCount int) []int {
message := make([]int, wordCount)
for i := 0; i < wordCount; i++ {
value := 0
for j := 0; j < wordSize; j++ {
if stuffedBits.GetBit(i*wordSize + j) {
value |= (1 << uint(wordSize-j-1))
}
}
message[i] = value
}
return message
}
func generateCheckWords(bits *utils.BitList, totalBits, wordSize int) *utils.BitList {
rs := utils.NewReedSolomonEncoder(getGF(wordSize))
// bits is guaranteed to be a multiple of the wordSize, so no padding needed
messageWordCount := bits.Len() / wordSize
totalWordCount := totalBits / wordSize
eccWordCount := totalWordCount - messageWordCount
messageWords := bitsToWords(bits, wordSize, messageWordCount)
eccWords := rs.Encode(messageWords, eccWordCount)
startPad := totalBits % wordSize
messageBits := new(utils.BitList)
messageBits.AddBits(0, byte(startPad))
for _, messageWord := range messageWords {
messageBits.AddBits(messageWord, byte(wordSize))
}
for _, eccWord := range eccWords {
messageBits.AddBits(eccWord, byte(wordSize))
}
return messageBits
}
func getGF(wordSize int) *utils.GaloisField {
switch wordSize {
case 4:
return utils.NewGaloisField(0x13, 16, 1)
case 6:
return utils.NewGaloisField(0x43, 64, 1)
case 8:
return utils.NewGaloisField(0x012D, 256, 1)
case 10:
return utils.NewGaloisField(0x409, 1024, 1)
case 12:
return utils.NewGaloisField(0x1069, 4096, 1)
default:
return nil
}
}