stuve-it-backend/goqr/qr_segment.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)
}