300 lines
8.2 KiB
Go
300 lines
8.2 KiB
Go
package goqr
|
|
|
|
import (
|
|
"errors"
|
|
"math"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Mode is the representation of the mode of a QR code character.
|
|
type Mode struct {
|
|
modeBits int // 4-bit mode indicator used in QR Code's data encoding
|
|
numBitsCharCount []int // number of bits used for character count indicator for different versions
|
|
}
|
|
|
|
// newMode creates a new Mode with a given mode indicator and character count bits
|
|
func newMode(mode int, ccbits ...int) Mode {
|
|
return Mode{
|
|
modeBits: mode,
|
|
numBitsCharCount: ccbits,
|
|
}
|
|
}
|
|
|
|
// numCharCountBits returns the number of character count bits
|
|
// for a specific QR code version.
|
|
func (m Mode) numCharCountBits(ver int) int {
|
|
return m.numBitsCharCount[(ver+7)/17]
|
|
}
|
|
|
|
// Predefined Mode values as defined by the QR Code standard.
|
|
var (
|
|
// Numeric mode is typically used for decimal digits (0 through 9).
|
|
Numeric = newMode(0x1, 10, 12, 14)
|
|
|
|
// Alphanumeric mode includes digits 0-9, uppercase letters A-Z and nine special characters.
|
|
Alphanumeric = newMode(0x2, 9, 11, 13)
|
|
|
|
// Byte mode can encode binary/byte data(default: ISO-8859-1)
|
|
Byte = newMode(0x4, 8, 16, 16) // Byte mode: binary/byte data (default: ISO-8859-1)
|
|
|
|
// Kanji mode is used for encoding Japanese Kanji characters.
|
|
Kanji = newMode(0x8, 8, 10, 12)
|
|
|
|
// Eci mode is designed for providing a method of extending features and functions
|
|
// in bar code symbols beyond those envisioned by the original standard.
|
|
Eci = newMode(0x7, 0, 0, 0)
|
|
)
|
|
|
|
// getModeBits function to return the bits representing a particular mode
|
|
func (m Mode) getModeBits() int {
|
|
return m.modeBits
|
|
}
|
|
|
|
// isNumeric checks if the mode is Numeric.
|
|
func (m Mode) isNumeric() bool {
|
|
return m.modeBits == Numeric.getModeBits()
|
|
}
|
|
|
|
// isAlphanumeric checks if the mode is Alphanumeric.
|
|
func (m Mode) isAlphanumeric() bool {
|
|
return m.modeBits == Alphanumeric.getModeBits()
|
|
}
|
|
|
|
// isByte checks if the mode is Byte.
|
|
func (m Mode) isByte() bool {
|
|
return m.modeBits == Byte.getModeBits()
|
|
}
|
|
|
|
// isKanji checks if the mode is Kanji.
|
|
func (m Mode) isKanji() bool {
|
|
return m.modeBits == Kanji.getModeBits()
|
|
}
|
|
|
|
// isEci checks if the mode is ECI.
|
|
func (m Mode) isEci() bool {
|
|
return m.modeBits == Eci.getModeBits()
|
|
}
|
|
|
|
var (
|
|
// numericRegex is a regular expression that matches strings consisting only of numbers (0-9).
|
|
numericRegex = regexp.MustCompile(`^\d+$`)
|
|
|
|
// alphanumericRegex is a regular expression that matches strings
|
|
// consisting only of numeric characters, uppercase alphabets, and certain special characters ($%*+-./:).
|
|
alphanumericRegex = regexp.MustCompile(`^[0-9A-Z $%*+\-.\/:]*$`)
|
|
|
|
// alphanumericCharset is a string listing all the characters that can be used in an alphanumeric string in QR codes.
|
|
alphanumericCharset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
|
|
)
|
|
|
|
// QrSegment is the representation of a segment of a QR code.
|
|
type QrSegment struct {
|
|
mode Mode // The mode of the QR segment (e.g., Numeric, Alphanumeric, etc.)
|
|
numChars int // Number of characters in the segment
|
|
data *BitBuffer // The actual binary data represented by this QR segment
|
|
}
|
|
|
|
// newQrSegment function creates a new QR segment with the given mode, number of characters, and data.
|
|
func newQrSegment(mode Mode, numCh int, data *BitBuffer) (*QrSegment, error) {
|
|
if numCh < 0 {
|
|
return nil, errors.New("invalid value")
|
|
}
|
|
return &QrSegment{
|
|
mode: mode,
|
|
numChars: numCh,
|
|
data: data.clone(), // Use a cloned copy of the data to prevent modifications
|
|
}, nil
|
|
}
|
|
|
|
// getData method clones and returns the BitBuffer data of the QR segment.
|
|
func (q *QrSegment) getData() *BitBuffer {
|
|
return q.data.clone()
|
|
}
|
|
|
|
// MakeBytes converts a byte slice into a QR segment in Byte mode.
|
|
// It returns an error if the input data is nil.
|
|
func MakeBytes(data []byte) (*QrSegment, error) {
|
|
if data == nil {
|
|
return nil, errors.New("data is nil")
|
|
}
|
|
|
|
bb := &BitBuffer{}
|
|
for _, b := range data {
|
|
err := bb.appendBits(int(b&0xFF), 8) // Append 8 bits at once to the bit buffer
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return newQrSegment(Byte, len(data), bb)
|
|
}
|
|
|
|
// MakeNumeric converts a string of digits into a QR code segment in Numeric mode
|
|
// It returns an error if the string contains non-numeric characters.
|
|
func MakeNumeric(digits string) (*QrSegment, error) {
|
|
if !isNumeric(digits) {
|
|
return nil, errors.New("string contains non-numeric characters")
|
|
}
|
|
|
|
bb := &BitBuffer{}
|
|
for i := 0; i < len(digits); {
|
|
n := min(len(digits)-i, 3) // find the length of the current chunk (up to 3 digits)
|
|
num, _ := strconv.Atoi(digits[i : i+n]) // convert the current chunk to an integer
|
|
err := bb.appendBits(num, n*3+1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
i += n // advance to the next chunk
|
|
}
|
|
|
|
return newQrSegment(Numeric, len(digits), bb)
|
|
}
|
|
|
|
// isNumeric function takes a string as input and returns a boolean indicating whether the string is numeric.
|
|
// It uses the MatchString method on the numericRegex to check the input string.
|
|
func isNumeric(numb string) bool {
|
|
return numericRegex.MatchString(numb)
|
|
}
|
|
|
|
// isAlphanumeric function takes a string as input and returns a boolean indicating whether the string is alphanumeric.
|
|
// It uses the MatchString method on the alphanumericRegex to check the input string.
|
|
func isAlphanumeric(text string) bool {
|
|
return alphanumericRegex.MatchString(text)
|
|
}
|
|
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func max(a, b int) int {
|
|
if a < b {
|
|
return b
|
|
}
|
|
return a
|
|
}
|
|
|
|
func abs(x int) int {
|
|
if x < 0 {
|
|
return -x
|
|
}
|
|
return x
|
|
}
|
|
|
|
// MakeAlphanumeric converts a string into a QR code segment in Alphanumeric mode
|
|
// It returns an error if the string contains non-alphanumeric characters.
|
|
func MakeAlphanumeric(text string) (*QrSegment, error) {
|
|
if !isAlphanumeric(text) {
|
|
return nil, errors.New("string contains unencodable characters in alphanumeric mode")
|
|
}
|
|
|
|
bb := &BitBuffer{}
|
|
i := 0
|
|
for ; i <= len(text)-2; i += 2 {
|
|
// Process each pair of characters in text.
|
|
temp := strings.IndexByte(alphanumericCharset, text[i]) * 45
|
|
temp += strings.IndexByte(alphanumericCharset, text[i+1])
|
|
err := bb.appendBits(temp, 11)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if i < len(text) {
|
|
err := bb.appendBits(strings.IndexByte(alphanumericCharset, text[i]), 6)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return newQrSegment(Alphanumeric, len(text), bb)
|
|
}
|
|
|
|
// MakeSegments converts data into QR segments based on the mode of text (Numeric, Alphanumeric or Byte, etc).
|
|
func MakeSegments(text string) ([]*QrSegment, error) {
|
|
res := make([]*QrSegment, 0)
|
|
if text == "" {
|
|
} else if isNumeric(text) {
|
|
seg, err := MakeNumeric(text)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res = append(res, seg)
|
|
} else if isAlphanumeric(text) {
|
|
seg, err := MakeAlphanumeric(text)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res = append(res, seg)
|
|
} else {
|
|
seg, err := MakeBytes([]byte(text))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res = append(res, seg)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// MakeEci converts an integer into a QR code segment in Eci mode
|
|
// It returns an error if the integer value is out of range.
|
|
func MakeEci(val int) (*QrSegment, error) {
|
|
bb := &BitBuffer{}
|
|
if val < 0 {
|
|
return nil, errors.New("ECI assignment value out of range")
|
|
} else if val < (1 << 7) {
|
|
err := bb.appendBits(val, 8)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if val < (1 << 14) {
|
|
err := bb.appendBits(0b10, 2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = bb.appendBits(val, 14)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if val < 1e6 {
|
|
err := bb.appendBits(0b110, 3)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = bb.appendBits(val, 21)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
return nil, errors.New("ECI assignment value out of range")
|
|
}
|
|
return newQrSegment(Eci, 0, bb)
|
|
}
|
|
|
|
// getTotalBits calculates and returns the total number of bits required to encode the segments at the specified QR version.
|
|
// It returns -1 if the number of characters exceeds the maximum capacity.
|
|
func getTotalBits(segs []*QrSegment, ver int) int {
|
|
var res int64
|
|
for _, seg := range segs {
|
|
if seg == nil {
|
|
continue
|
|
}
|
|
|
|
ccbits := seg.mode.numCharCountBits(ver)
|
|
if seg.numChars >= (1 << ccbits) {
|
|
return -1
|
|
}
|
|
res += int64(4 + ccbits + seg.data.len())
|
|
if res > math.MaxInt32 {
|
|
return -1
|
|
}
|
|
}
|
|
return int(res)
|
|
}
|