Introduce QR code generation & serve it via the media server (#3154)
* introduce QR code generation
This commit is contained in:
parent
550de3bff2
commit
efee11d28a
3
go.mod
3
go.mod
|
@ -78,6 +78,8 @@ require (
|
||||||
github.com/meirf/gopart v0.0.0-20180520194036-37e9492a85a8
|
github.com/meirf/gopart v0.0.0-20180520194036-37e9492a85a8
|
||||||
github.com/rmg/iso4217 v1.0.0
|
github.com/rmg/iso4217 v1.0.0
|
||||||
github.com/waku-org/go-waku v0.4.1-0.20230131145040-6169a44c242f
|
github.com/waku-org/go-waku v0.4.1-0.20230131145040-6169a44c242f
|
||||||
|
github.com/yeqown/go-qrcode/v2 v2.2.1
|
||||||
|
github.com/yeqown/go-qrcode/writer/standard v1.2.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -243,6 +245,7 @@ require (
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
|
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||||
go.etcd.io/bbolt v1.3.6 // indirect
|
go.etcd.io/bbolt v1.3.6 // indirect
|
||||||
go.opencensus.io v0.23.0 // indirect
|
go.opencensus.io v0.23.0 // indirect
|
||||||
go.uber.org/atomic v1.10.0 // indirect
|
go.uber.org/atomic v1.10.0 // indirect
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -2104,6 +2104,12 @@ github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6Ut
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||||
|
github.com/yeqown/go-qrcode/v2 v2.2.1 h1:Jc1Q916fwC05R8C7mpWDbrT9tyLPaLLKDABoC5XBCe8=
|
||||||
|
github.com/yeqown/go-qrcode/v2 v2.2.1/go.mod h1:2Qsk2APUCPne0TsRo40DIkI5MYnbzYKCnKGEFWrxd24=
|
||||||
|
github.com/yeqown/go-qrcode/writer/standard v1.2.1 h1:FMRZiur5yApUIe4fqtqmcdl/XQTZAZWt2DhkPx4VIW0=
|
||||||
|
github.com/yeqown/go-qrcode/writer/standard v1.2.1/go.mod h1:ZelyDFiVymrauRjUn454iF7bjsabmB1vixkDA5kq2bw=
|
||||||
|
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
||||||
|
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
|
|
@ -0,0 +1,182 @@
|
||||||
|
package qrcode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
"image/png"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
xdraw "golang.org/x/image/draw"
|
||||||
|
|
||||||
|
"github.com/status-im/status-go/multiaccounts"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultPadding = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetImageDimensions(imgBytes []byte) (int, int, error) {
|
||||||
|
// Decode image bytes
|
||||||
|
img, _, err := image.Decode(bytes.NewReader(imgBytes))
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
// Get the image dimensions
|
||||||
|
bounds := img.Bounds()
|
||||||
|
width := bounds.Max.X - bounds.Min.X
|
||||||
|
height := bounds.Max.Y - bounds.Min.Y
|
||||||
|
return width, height, nil
|
||||||
|
}
|
||||||
|
func ToLogoImageFromBytes(imageBytes []byte, padding int) []byte {
|
||||||
|
img, _, err := image.Decode(bytes.NewReader(imageBytes))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
bounds := img.Bounds()
|
||||||
|
newBounds := image.Rect(bounds.Min.X-padding, bounds.Min.Y-padding, bounds.Max.X+padding, bounds.Max.Y+padding)
|
||||||
|
white := image.NewRGBA(newBounds)
|
||||||
|
draw.Draw(white, newBounds, &image.Uniform{C: color.White}, image.ZP, draw.Src)
|
||||||
|
// Create a circular mask
|
||||||
|
circle := image.NewRGBA(bounds)
|
||||||
|
draw.DrawMask(circle, bounds, img, image.ZP, &Circle{
|
||||||
|
X: bounds.Dx() / 2,
|
||||||
|
Y: bounds.Dy() / 2,
|
||||||
|
R: bounds.Dx() / 2,
|
||||||
|
}, image.ZP, draw.Over)
|
||||||
|
// Calculate the center point of the new image
|
||||||
|
centerX := (newBounds.Min.X + newBounds.Max.X) / 2
|
||||||
|
centerY := (newBounds.Min.Y + newBounds.Max.Y) / 2
|
||||||
|
// Draw the circular image in the center of the new image
|
||||||
|
draw.Draw(white, bounds.Add(image.Pt(centerX-bounds.Dx()/2, centerY-bounds.Dy()/2)), circle, image.ZP, draw.Over)
|
||||||
|
// Encode image to png format and save in a bytes
|
||||||
|
var resultImg bytes.Buffer
|
||||||
|
err = png.Encode(&resultImg, white)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
resultBytes := resultImg.Bytes()
|
||||||
|
return resultBytes
|
||||||
|
}
|
||||||
|
func SuperimposeImage(imageBytes []byte, qrFilepath []byte) []byte {
|
||||||
|
// Read the two images from bytes
|
||||||
|
img1, _, _ := image.Decode(bytes.NewReader(imageBytes))
|
||||||
|
img2, _, _ := image.Decode(bytes.NewReader(qrFilepath))
|
||||||
|
// Create a new image with the dimensions of the first image
|
||||||
|
result := image.NewRGBA(img1.Bounds())
|
||||||
|
// Draw the first image on the new image
|
||||||
|
draw.Draw(result, img1.Bounds(), img1, image.ZP, draw.Src)
|
||||||
|
// Get the dimensions of the second image
|
||||||
|
img2Bounds := img2.Bounds()
|
||||||
|
// Calculate the x and y coordinates to center the second image
|
||||||
|
x := (img1.Bounds().Dx() - img2Bounds.Dx()) / 2
|
||||||
|
y := (img1.Bounds().Dy() - img2Bounds.Dy()) / 2
|
||||||
|
// Draw the second image on top of the first image at the calculated coordinates
|
||||||
|
draw.Draw(result, img2Bounds.Add(image.Pt(x, y)), img2, image.ZP, draw.Over)
|
||||||
|
// Encode the final image to a desired format
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := png.Encode(&b, result); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
func ResizeImage(imgBytes []byte, width, height int) ([]byte, error) {
|
||||||
|
// Decode image bytes
|
||||||
|
img, _, err := image.Decode(bytes.NewReader(imgBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Create a new image with the desired dimensions
|
||||||
|
newImg := image.NewNRGBA(image.Rect(0, 0, width, height))
|
||||||
|
xdraw.BiLinear.Scale(newImg, newImg.Bounds(), img, img.Bounds(), draw.Over, nil)
|
||||||
|
// Encode the new image to bytes
|
||||||
|
var newImgBytes bytes.Buffer
|
||||||
|
if err = png.Encode(&newImgBytes, newImg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newImgBytes.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ImageToBytes(imagePath string) ([]byte, error) {
|
||||||
|
// Open the image file
|
||||||
|
file, err := os.Open(imagePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Decode the image
|
||||||
|
img, _, err := image.Decode(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new buffer to hold the image data
|
||||||
|
var imgBuffer bytes.Buffer
|
||||||
|
|
||||||
|
// Encode the image to the desired format and save it in the buffer
|
||||||
|
err = png.Encode(&imgBuffer, img)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the image data as a byte slice
|
||||||
|
return imgBuffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLogoImage(multiaccountsDB *multiaccounts.Database, params url.Values) ([]byte, error) {
|
||||||
|
var imageFolderBasePath = "../_assets/tests/"
|
||||||
|
var logoFileStaticPath = imageFolderBasePath + "status.png"
|
||||||
|
|
||||||
|
keyUids, ok := params["keyUid"]
|
||||||
|
if !ok || len(keyUids) == 0 {
|
||||||
|
return nil, errors.New("no keyUid")
|
||||||
|
}
|
||||||
|
imageNames, ok := params["imageName"]
|
||||||
|
if !ok || len(imageNames) == 0 {
|
||||||
|
return nil, errors.New("no imageName")
|
||||||
|
}
|
||||||
|
identityImageObjectFromDB, err := multiaccountsDB.GetIdentityImage(keyUids[0], imageNames[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
staticLogoFileBytes, _ := ImageToBytes(logoFileStaticPath)
|
||||||
|
|
||||||
|
if identityImageObjectFromDB == nil {
|
||||||
|
return ToLogoImageFromBytes(staticLogoFileBytes, GetPadding(staticLogoFileBytes)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ToLogoImageFromBytes(identityImageObjectFromDB.Payload, GetPadding(identityImageObjectFromDB.Payload)), nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPadding(imgBytes []byte) int {
|
||||||
|
size, _, err := GetImageDimensions(imgBytes)
|
||||||
|
if err != nil {
|
||||||
|
return defaultPadding
|
||||||
|
}
|
||||||
|
return size / 5
|
||||||
|
}
|
||||||
|
|
||||||
|
type Circle struct {
|
||||||
|
X, Y, R int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Circle) ColorModel() color.Model {
|
||||||
|
return color.AlphaModel
|
||||||
|
}
|
||||||
|
func (c *Circle) Bounds() image.Rectangle {
|
||||||
|
return image.Rect(c.X-c.R, c.Y-c.R, c.X+c.R, c.Y+c.R)
|
||||||
|
}
|
||||||
|
func (c *Circle) At(x, y int) color.Color {
|
||||||
|
xx, yy, rr := float64(x-c.X)+0.5, float64(y-c.Y)+0.5, float64(c.R)
|
||||||
|
if xx*xx+yy*yy < rr*rr {
|
||||||
|
return color.Alpha{255}
|
||||||
|
}
|
||||||
|
return color.Alpha{0}
|
||||||
|
}
|
|
@ -3,12 +3,15 @@ package server
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/base64"
|
||||||
"image"
|
"image"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
qrcode "github.com/yeqown/go-qrcode/v2"
|
||||||
|
"github.com/yeqown/go-qrcode/writer/standard"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/status-im/status-go/ipfs"
|
"github.com/status-im/status-go/ipfs"
|
||||||
|
@ -17,6 +20,7 @@ import (
|
||||||
"github.com/status-im/status-go/protocol/identity/identicon"
|
"github.com/status-im/status-go/protocol/identity/identicon"
|
||||||
"github.com/status-im/status-go/protocol/identity/ring"
|
"github.com/status-im/status-go/protocol/identity/ring"
|
||||||
"github.com/status-im/status-go/protocol/images"
|
"github.com/status-im/status-go/protocol/images"
|
||||||
|
qrcodeutils "github.com/status-im/status-go/qrcode"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -31,10 +35,30 @@ const (
|
||||||
// Handler routes for pairing
|
// Handler routes for pairing
|
||||||
accountImagesPath = "/accountImages"
|
accountImagesPath = "/accountImages"
|
||||||
contactImagesPath = "/contactImages"
|
contactImagesPath = "/contactImages"
|
||||||
|
generateQRCode = "/GenerateQRCode"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HandlerPatternMap map[string]http.HandlerFunc
|
type HandlerPatternMap map[string]http.HandlerFunc
|
||||||
|
|
||||||
|
type QROptions struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
ErrorCorrectionLevel string `json:"errorCorrectionLevel"`
|
||||||
|
Capacity string `json:"capacity"`
|
||||||
|
AllowProfileImage bool `json:"withLogo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WriterCloserByteBuffer struct {
|
||||||
|
*bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wc WriterCloserByteBuffer) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWriterCloserByteBuffer() *WriterCloserByteBuffer {
|
||||||
|
return &WriterCloserByteBuffer{bytes.NewBuffer([]byte{})}
|
||||||
|
}
|
||||||
|
|
||||||
func handleAccountImages(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc {
|
func handleAccountImages(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
params := r.URL.Query()
|
params := r.URL.Query()
|
||||||
|
@ -384,3 +408,68 @@ func handleIPFS(downloader *ipfs.Downloader, logger *zap.Logger) http.HandlerFun
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleQRCodeGeneration(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
params := r.URL.Query()
|
||||||
|
qrURLBase64Encoded, ok := params["qrurl"]
|
||||||
|
if !ok || len(qrURLBase64Encoded) == 0 {
|
||||||
|
logger.Error("no qr url provided")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
qrURLBase64Decoded, err := base64.StdEncoding.DecodeString(qrURLBase64Encoded[0])
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("error decoding string from base64", zap.Error(err))
|
||||||
|
}
|
||||||
|
level, ok := params["level"]
|
||||||
|
// Default error correction level
|
||||||
|
correctionLevel := qrcode.ErrorCorrectionMedium
|
||||||
|
if ok && len(level) == 1 {
|
||||||
|
switch level[0] {
|
||||||
|
case "4":
|
||||||
|
correctionLevel = qrcode.ErrorCorrectionHighest
|
||||||
|
case "1":
|
||||||
|
correctionLevel = qrcode.ErrorCorrectionLow
|
||||||
|
case "3":
|
||||||
|
correctionLevel = qrcode.ErrorCorrectionQuart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf := NewWriterCloserByteBuffer()
|
||||||
|
qrc, err := qrcode.NewWith(string(qrURLBase64Decoded),
|
||||||
|
qrcode.WithEncodingMode(qrcode.EncModeAuto),
|
||||||
|
qrcode.WithErrorCorrectionLevel(correctionLevel),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("could not generate QRCode", zap.Error(err))
|
||||||
|
}
|
||||||
|
nw := standard.NewWithWriter(buf)
|
||||||
|
if err = qrc.Save(nw); err != nil {
|
||||||
|
logger.Error("could not save image", zap.Error(err))
|
||||||
|
}
|
||||||
|
payload := buf.Bytes()
|
||||||
|
logo, err := qrcodeutils.GetLogoImage(multiaccountsDB, params)
|
||||||
|
if err == nil {
|
||||||
|
qrWidth, qrHeight, _ := qrcodeutils.GetImageDimensions(payload)
|
||||||
|
logo, _ = qrcodeutils.ResizeImage(logo, qrWidth/5, qrHeight/5)
|
||||||
|
payload = qrcodeutils.SuperimposeImage(payload, logo)
|
||||||
|
}
|
||||||
|
size, ok := params["size"]
|
||||||
|
if ok && len(size) == 1 {
|
||||||
|
size, err := strconv.Atoi(size[0])
|
||||||
|
if err == nil {
|
||||||
|
payload, _ = qrcodeutils.ResizeImage(payload, size, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mime, err := images.ImageMime(payload)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("could not generate image from payload", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", mime)
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
_, err = w.Write(payload)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to write image", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ func NewMediaServer(db *sql.DB, downloader *ipfs.Downloader, multiaccountsDB *mu
|
||||||
contactImagesPath: handleContactImages(s.db, s.logger),
|
contactImagesPath: handleContactImages(s.db, s.logger),
|
||||||
discordAuthorsPath: handleDiscordAuthorAvatar(s.db, s.logger),
|
discordAuthorsPath: handleDiscordAuthorAvatar(s.db, s.logger),
|
||||||
discordAttachmentsPath: handleDiscordAttachment(s.db, s.logger),
|
discordAttachmentsPath: handleDiscordAttachment(s.db, s.logger),
|
||||||
|
generateQRCode: handleQRCodeGeneration(s.multiaccountsDB, s.logger),
|
||||||
})
|
})
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
|
@ -103,3 +104,11 @@ func (s *MediaServer) MakeStickerURL(stickerHash string) string {
|
||||||
|
|
||||||
return u.String()
|
return u.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *MediaServer) MakeQRURL(qurul string) string {
|
||||||
|
u := s.MakeBaseURL()
|
||||||
|
u.Path = generateQRCode
|
||||||
|
u.RawQuery = url.Values{"qurul": {qurul}}.Encode()
|
||||||
|
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
vendor/
|
||||||
|
draft/
|
||||||
|
testdata/
|
||||||
|
js/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# example/
|
||||||
|
*.png
|
||||||
|
default.jpeg
|
||||||
|
# *.json
|
||||||
|
*.log
|
||||||
|
.DS_store
|
||||||
|
tmp.png
|
||||||
|
|
||||||
|
go.sum
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 yeqown
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,14 @@
|
||||||
|
release: release-osx release-linux
|
||||||
|
|
||||||
|
release-osx:
|
||||||
|
- mkdir -p draft/osx
|
||||||
|
GOOS=darwin GOARCH=amd64 go build -o draft/osx/go-qrcode ./cmd/go-qrcode.go
|
||||||
|
cd draft/osx && tar -zcvf ../go-qrcode.osx.tar.gz .
|
||||||
|
|
||||||
|
release-linux:
|
||||||
|
- mkdir -p draft/linux
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o draft/linux/go-qrcode ./cmd/go-qrcode.go
|
||||||
|
cd draft/linux && tar -zcvf ../go-qrcode.linux.tar.gz .
|
||||||
|
|
||||||
|
test-all:
|
||||||
|
go test -v --count=1 ./...
|
|
@ -0,0 +1,139 @@
|
||||||
|
# go-qrcode #
|
||||||
|
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/github.com/yeqown/go-qrcode)](https://goreportcard.com/report/github.com/yeqown/go-qrcode)
|
||||||
|
[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/yeqown/go-qrcode/v2)
|
||||||
|
[![Go](https://github.com/yeqown/go-qrcode/actions/workflows/go.yml/badge.svg?branch=main)](https://github.com/yeqown/go-qrcode/actions/workflows/go.yml) ![](https://changkun.de/urlstat?mode=github&repo=yeqown/go-qrcode)
|
||||||
|
![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/yeqown/go-qrcode)
|
||||||
|
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/yeqown/go-qrcode)
|
||||||
|
[![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
|
||||||
|
|
||||||
|
<img src="./assets/repository_qrcode.png" width="100px" align="right"/>
|
||||||
|
QR code (abbreviated from Quick Response Code) is the trademark for a type of matrix barcode (or two-dimensional barcode) first designed in 1994 for the automotive industry in Japan. A barcode is a machine-readable optical label that contains information about the item to which it is attached. A QR code uses four standardized encoding modes (numeric, alphanumeric, byte/binary, and kanji) to store data efficiently; extensions may also be used
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- [x] Normally generate QR code across `version 1` to `version 40`.
|
||||||
|
- [x] Automatically analyze QR version by source text.
|
||||||
|
- [x] Specifying cell shape allowably with `WithCustomShape`, `WithCircleShape` (default is `rectangle`)
|
||||||
|
- [x] Specifying output file's format with `WithBuiltinImageEncoder`, `WithCustomImageEncoder` (default is `JPEG`)
|
||||||
|
- [x] Not only shape of cell, but also color of QR Code background and foreground color.
|
||||||
|
- [x] `WithLogoImage`, `WithLogoImageFilePNG`, `WithLogoImageFileJPEG` help you add an icon at the central of QR Code.
|
||||||
|
- [x] `WithBorderWidth` allows to specify any width of 4 sides around the qrcode.
|
||||||
|
- [x] `WebAssembly` support, check out the [Example](./example/webassembly/README.md) and [README](cmd/wasm/README.md) for more detail.
|
||||||
|
- [x] support Halftone QR Codes, check out the [Example](./example/with-halftone).
|
||||||
|
### Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get -u github.com/yeqown/go-qrcode/v2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
link to [CODE](./example/main.go)
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yeqown/go-qrcode/v2"
|
||||||
|
"github.com/yeqown/go-qrcode/writer/standard"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
qrc, err := qrcode.New("https://github.com/yeqown/go-qrcode")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("could not generate QRCode: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := standard.New("../assets/repo-qrcode.jpeg")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("standard.New failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// save file
|
||||||
|
if err = qrc.Save(w); err != nil {
|
||||||
|
fmt.Printf("could not save image: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```go
|
||||||
|
const (
|
||||||
|
// EncModeNone mode ...
|
||||||
|
EncModeNone encMode = 1 << iota
|
||||||
|
// EncModeNumeric mode ...
|
||||||
|
EncModeNumeric
|
||||||
|
// EncModeAlphanumeric mode ...
|
||||||
|
EncModeAlphanumeric
|
||||||
|
// EncModeByte mode ...
|
||||||
|
EncModeByte
|
||||||
|
// EncModeJP mode ...
|
||||||
|
EncModeJP
|
||||||
|
)
|
||||||
|
|
||||||
|
// WithEncodingMode sets the encoding mode.
|
||||||
|
func WithEncodingMode(mode encMode) EncodeOption {}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ErrorCorrectionLow :Level L: 7% error recovery.
|
||||||
|
ErrorCorrectionLow ecLevel = iota + 1
|
||||||
|
|
||||||
|
// ErrorCorrectionMedium :Level M: 15% error recovery. Good default choice.
|
||||||
|
ErrorCorrectionMedium
|
||||||
|
|
||||||
|
// ErrorCorrectionQuart :Level Q: 25% error recovery.
|
||||||
|
ErrorCorrectionQuart
|
||||||
|
|
||||||
|
// ErrorCorrectionHighest :Level H: 30% error recovery.
|
||||||
|
ErrorCorrectionHighest
|
||||||
|
)
|
||||||
|
|
||||||
|
// WithErrorCorrectionLevel sets the error correction level.
|
||||||
|
func WithErrorCorrectionLevel(ecLevel ecLevel) EncodeOption {}
|
||||||
|
```
|
||||||
|
|
||||||
|
following are some shots:
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<img src="./assets/example_fg_bg.jpeg" width="160px" align="left" title="with bg-fg color">
|
||||||
|
<img src="./assets/example_logo.jpeg" width="160px" align="left" title="with logo image">
|
||||||
|
<img src="./assets/example_circle.jpeg" width="160px" align="left" title="customize block shape">
|
||||||
|
<img src="./assets/example_transparent.png" width="160px" title="with transparent bg">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<img src="./assets/example_halftone0.jpeg" width="160px" align="left" title="halftone0">
|
||||||
|
<img src="./assets/example_halftone1.jpeg" width="160px" align="left" title="halftone1">
|
||||||
|
<img src="./assets/example_halftone2.jpeg" width="160px" align="left" title="halftone2">
|
||||||
|
<img src="./assets/example_halftone3.jpeg" width="160px" title="halftone3">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Built-in Writers
|
||||||
|
|
||||||
|
- [Standard Writer](./writer/standard/README.md), prints QRCode into file and stream
|
||||||
|
- [Terminal Writer](./writer/terminal/README.md), prints QRCode into terminal
|
||||||
|
|
||||||
|
Of course, you can also code your own writer, just implement [Writer](./writer/README.md) interface.
|
||||||
|
|
||||||
|
### Migrating from v1
|
||||||
|
|
||||||
|
`go-qrcode.v2` is a major upgrade from v1, and it is not backward compatible. `v2` redesigned
|
||||||
|
the API, and it is more flexible and powerful. Features are split into different modules (according to functionality).
|
||||||
|
|
||||||
|
- github.com/yeqown/go-qrcode/v2 **_core_**
|
||||||
|
- github.com/yeqown/go-qrcode/writer/standard **_writer/imageFile_**
|
||||||
|
- github.com/yeqown/go-qrcode/writer/terminal **_writer/terminal_**
|
||||||
|
|
||||||
|
Check [example/migrating-from-v1](./example/migrating-from-v1/main.go) for more details.
|
||||||
|
|
||||||
|
### Links
|
||||||
|
|
||||||
|
* [QRCode Tourist](https://www.thonky.com/qr-code-tutorial/)
|
||||||
|
* [QRCode Wiki](https://en.wikipedia.org/wiki/QR_code)
|
||||||
|
* [二维码详解 (QRCode analysis in CN-zh)](https://zhuanlan.zhihu.com/p/21463650)
|
||||||
|
* [数据编码 (How to encode data payload in QRCode in CN-zh)](https://zhuanlan.zhihu.com/p/25432676)
|
|
@ -0,0 +1,109 @@
|
||||||
|
package qrcode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/jpeg"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// _debug mode switch, true means enable debug mode, false means disable.
|
||||||
|
_debug = false
|
||||||
|
_debugOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func debugEnabled() bool {
|
||||||
|
// load debug switch from environment only once.
|
||||||
|
_debugOnce.Do(func() {
|
||||||
|
switch os.Getenv("QRCODE_DEBUG") {
|
||||||
|
case "1", "true", "TRUE", "enabled", "ENABLED":
|
||||||
|
_debug = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return _debug
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDebugMode open debug switch, you can also enable debug by runtime
|
||||||
|
// environments variables: QRCODE_DEBUG=1 [1, true, TRUE, enabled, ENABLED] which is recommended.
|
||||||
|
func SetDebugMode() {
|
||||||
|
_debug = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugLogf(format string, v ...interface{}) {
|
||||||
|
if !debugEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[qrcode] DEBUG: "+format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugDraw(filename string, mat Matrix) error {
|
||||||
|
if !debugEnabled() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fd, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("debugDraw open file %s failed: %w", filename, err)
|
||||||
|
}
|
||||||
|
defer func(fd *os.File) {
|
||||||
|
_ = fd.Close()
|
||||||
|
}(fd)
|
||||||
|
|
||||||
|
return debugDrawTo(fd, mat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugDrawTo(w io.Writer, mat Matrix) error {
|
||||||
|
if !debugEnabled() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// width as image width, height as image height
|
||||||
|
padding := 10
|
||||||
|
blockWidth := 10
|
||||||
|
width := mat.Width()*blockWidth + 2*padding
|
||||||
|
height := width
|
||||||
|
img := image.NewGray16(image.Rect(0, 0, width, height))
|
||||||
|
|
||||||
|
rectangle := func(x1, y1 int, x2, y2 int, img *image.Gray16, c color.Gray16) {
|
||||||
|
for x := x1; x < x2; x++ {
|
||||||
|
for y := y1; y < y2; y++ {
|
||||||
|
img.SetGray16(x, y, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// background
|
||||||
|
rectangle(0, 0, width, height, img, color.Gray16{Y: 0xff12})
|
||||||
|
|
||||||
|
mat.iter(IterDirection_COLUMN, func(x int, y int, v qrvalue) {
|
||||||
|
sx := x*blockWidth + padding
|
||||||
|
sy := y*blockWidth + padding
|
||||||
|
es := (x+1)*blockWidth + padding
|
||||||
|
ey := (y+1)*blockWidth + padding
|
||||||
|
|
||||||
|
// choose color, false use black, others use black on white background
|
||||||
|
var gray color.Gray16
|
||||||
|
switch v.qrbool() {
|
||||||
|
case false:
|
||||||
|
gray = color.White
|
||||||
|
default:
|
||||||
|
gray = color.Black
|
||||||
|
}
|
||||||
|
|
||||||
|
rectangle(sx, sy, es, ey, img, gray)
|
||||||
|
})
|
||||||
|
|
||||||
|
// save to writer
|
||||||
|
err := jpeg.Encode(w, img, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("debugDrawTo: encode image in JPEG failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,350 @@
|
||||||
|
// Package qrcode ...
|
||||||
|
// encoder.go working for data encoding
|
||||||
|
package qrcode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/yeqown/reedsolomon/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
// encMode ...
|
||||||
|
type encMode uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
// a qrbool of EncModeAuto will trigger a detection of the letter set from the input data,
|
||||||
|
EncModeAuto = 0
|
||||||
|
// EncModeNone mode ...
|
||||||
|
EncModeNone encMode = 1 << iota
|
||||||
|
// EncModeNumeric mode ...
|
||||||
|
EncModeNumeric
|
||||||
|
// EncModeAlphanumeric mode ...
|
||||||
|
EncModeAlphanumeric
|
||||||
|
// EncModeByte mode ...
|
||||||
|
EncModeByte
|
||||||
|
// EncModeJP mode ...
|
||||||
|
EncModeJP
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
paddingByte1, _ = binary.NewFromBinaryString("11101100")
|
||||||
|
paddingByte2, _ = binary.NewFromBinaryString("00010001")
|
||||||
|
)
|
||||||
|
|
||||||
|
// getEncModeName ...
|
||||||
|
func getEncModeName(mode encMode) string {
|
||||||
|
switch mode {
|
||||||
|
case EncModeNone:
|
||||||
|
return "none"
|
||||||
|
case EncModeNumeric:
|
||||||
|
return "numeric"
|
||||||
|
case EncModeAlphanumeric:
|
||||||
|
return "alphanumeric"
|
||||||
|
case EncModeByte:
|
||||||
|
return "byte"
|
||||||
|
case EncModeJP:
|
||||||
|
return "japan"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEncodeModeIndicator ...
|
||||||
|
func getEncodeModeIndicator(mode encMode) *binary.Binary {
|
||||||
|
switch mode {
|
||||||
|
case EncModeNumeric:
|
||||||
|
return binary.New(false, false, false, true)
|
||||||
|
case EncModeAlphanumeric:
|
||||||
|
return binary.New(false, false, true, false)
|
||||||
|
case EncModeByte:
|
||||||
|
return binary.New(false, true, false, false)
|
||||||
|
case EncModeJP:
|
||||||
|
return binary.New(true, false, false, false)
|
||||||
|
default:
|
||||||
|
panic("no indicator")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// encoder ... data to bit stream ...
|
||||||
|
type encoder struct {
|
||||||
|
// self init
|
||||||
|
dst *binary.Binary
|
||||||
|
data []byte // raw input data
|
||||||
|
|
||||||
|
// initial params
|
||||||
|
mode encMode // encode mode
|
||||||
|
ecLv ecLevel // error correction level
|
||||||
|
|
||||||
|
// self load
|
||||||
|
version version // QR version ref
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEncoder(m encMode, ec ecLevel, v version) *encoder {
|
||||||
|
return &encoder{
|
||||||
|
dst: nil,
|
||||||
|
data: nil,
|
||||||
|
mode: m,
|
||||||
|
ecLv: ec,
|
||||||
|
version: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode ...
|
||||||
|
// 1. encode raw data into bitset
|
||||||
|
// 2. append _defaultPadding data
|
||||||
|
//
|
||||||
|
func (e *encoder) Encode(byts []byte) (*binary.Binary, error) {
|
||||||
|
e.dst = binary.New()
|
||||||
|
e.data = byts
|
||||||
|
|
||||||
|
// append mode indicator symbol
|
||||||
|
indicator := getEncodeModeIndicator(e.mode)
|
||||||
|
e.dst.Append(indicator)
|
||||||
|
// append chars length counter bits symbol
|
||||||
|
e.dst.AppendUint32(uint32(len(byts)), e.charCountBits())
|
||||||
|
|
||||||
|
// encode data with specified mode
|
||||||
|
switch e.mode {
|
||||||
|
case EncModeNumeric:
|
||||||
|
e.encodeNumeric()
|
||||||
|
case EncModeAlphanumeric:
|
||||||
|
e.encodeAlphanumeric()
|
||||||
|
case EncModeByte:
|
||||||
|
e.encodeByte()
|
||||||
|
case EncModeJP:
|
||||||
|
panic("this has not been finished")
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill and _defaultPadding bits
|
||||||
|
e.breakUpInto8bit()
|
||||||
|
|
||||||
|
return e.dst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0001b mode indicator
|
||||||
|
func (e *encoder) encodeNumeric() {
|
||||||
|
if e.dst == nil {
|
||||||
|
log.Println("e.dst is nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := 0; i < len(e.data); i += 3 {
|
||||||
|
charsRemaining := len(e.data) - i
|
||||||
|
|
||||||
|
var value uint32
|
||||||
|
bitsUsed := 1
|
||||||
|
|
||||||
|
for j := 0; j < charsRemaining && j < 3; j++ {
|
||||||
|
value *= 10
|
||||||
|
value += uint32(e.data[i+j] - 0x30)
|
||||||
|
bitsUsed += 3
|
||||||
|
}
|
||||||
|
e.dst.AppendUint32(value, bitsUsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0010b mode indicator
|
||||||
|
func (e *encoder) encodeAlphanumeric() {
|
||||||
|
if e.dst == nil {
|
||||||
|
log.Println("e.dst is nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := 0; i < len(e.data); i += 2 {
|
||||||
|
charsRemaining := len(e.data) - i
|
||||||
|
|
||||||
|
var value uint32
|
||||||
|
for j := 0; j < charsRemaining && j < 2; j++ {
|
||||||
|
value *= 45
|
||||||
|
value += encodeAlphanumericCharacter(e.data[i+j])
|
||||||
|
}
|
||||||
|
|
||||||
|
bitsUsed := 6
|
||||||
|
if charsRemaining > 1 {
|
||||||
|
bitsUsed = 11
|
||||||
|
}
|
||||||
|
|
||||||
|
e.dst.AppendUint32(value, bitsUsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0100b mode indicator
|
||||||
|
func (e *encoder) encodeByte() {
|
||||||
|
if e.dst == nil {
|
||||||
|
log.Println("e.dst is nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, b := range e.data {
|
||||||
|
_ = e.dst.AppendByte(b, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Break Up into 8-bit Codewords and Add Pad Bytes if Necessary
|
||||||
|
func (e *encoder) breakUpInto8bit() {
|
||||||
|
// fill ending code (max 4bit)
|
||||||
|
// depends on max capacity of current version and EC level
|
||||||
|
maxCap := e.version.NumTotalCodewords() * 8
|
||||||
|
if less := maxCap - e.dst.Len(); less < 0 {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"wrong version(%d) cap(%d bits) and could not contain all bits: %d bits",
|
||||||
|
e.version.Ver, maxCap, e.dst.Len(),
|
||||||
|
)
|
||||||
|
panic(err)
|
||||||
|
} else if less < 4 {
|
||||||
|
e.dst.AppendNumBools(less, false)
|
||||||
|
} else {
|
||||||
|
e.dst.AppendNumBools(4, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// append `0` to be 8 times bits length
|
||||||
|
if mod := e.dst.Len() % 8; mod != 0 {
|
||||||
|
e.dst.AppendNumBools(8-mod, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// _defaultPadding bytes
|
||||||
|
// _defaultPadding byte 11101100 00010001
|
||||||
|
if n := maxCap - e.dst.Len(); n > 0 {
|
||||||
|
debugLogf("maxCap: %d, len: %d, less: %d", maxCap, e.dst.Len(), n)
|
||||||
|
for i := 1; i <= (n / 8); i++ {
|
||||||
|
if i%2 == 1 {
|
||||||
|
e.dst.Append(paddingByte1)
|
||||||
|
} else {
|
||||||
|
e.dst.Append(paddingByte2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字符计数指示符位长字典
|
||||||
|
var charCountMap = map[string]int{
|
||||||
|
"9_numeric": 10,
|
||||||
|
"9_alphanumeric": 9,
|
||||||
|
"9_byte": 8,
|
||||||
|
"9_japan": 8,
|
||||||
|
"26_numeric": 12,
|
||||||
|
"26_alphanumeric": 11,
|
||||||
|
"26_byte": 16,
|
||||||
|
"26_japan": 10,
|
||||||
|
"40_numeric": 14,
|
||||||
|
"40_alphanumeric": 13,
|
||||||
|
"40_byte": 16,
|
||||||
|
"40_japan": 12,
|
||||||
|
}
|
||||||
|
|
||||||
|
// charCountBits
|
||||||
|
func (e *encoder) charCountBits() int {
|
||||||
|
var lv int
|
||||||
|
if v := e.version.Ver; v <= 9 {
|
||||||
|
lv = 9
|
||||||
|
} else if v <= 26 {
|
||||||
|
lv = 26
|
||||||
|
} else {
|
||||||
|
lv = 40
|
||||||
|
}
|
||||||
|
pos := fmt.Sprintf("%d_%s", lv, getEncModeName(e.mode))
|
||||||
|
return charCountMap[pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
// v must be a QR Code defined alphanumeric character: 0-9, A-Z, SP, $%*+-./ or
|
||||||
|
// :. The characters are mapped to values in the range 0-44 respectively.
|
||||||
|
func encodeAlphanumericCharacter(v byte) uint32 {
|
||||||
|
c := uint32(v)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case c >= '0' && c <= '9':
|
||||||
|
// 0-9 encoded as 0-9.
|
||||||
|
return c - '0'
|
||||||
|
case c >= 'A' && c <= 'Z':
|
||||||
|
// A-Z encoded as 10-35.
|
||||||
|
return c - 'A' + 10
|
||||||
|
case c == ' ':
|
||||||
|
return 36
|
||||||
|
case c == '$':
|
||||||
|
return 37
|
||||||
|
case c == '%':
|
||||||
|
return 38
|
||||||
|
case c == '*':
|
||||||
|
return 39
|
||||||
|
case c == '+':
|
||||||
|
return 40
|
||||||
|
case c == '-':
|
||||||
|
return 41
|
||||||
|
case c == '.':
|
||||||
|
return 42
|
||||||
|
case c == '/':
|
||||||
|
return 43
|
||||||
|
case c == ':':
|
||||||
|
return 44
|
||||||
|
default:
|
||||||
|
log.Panicf("encodeAlphanumericCharacter() with non alphanumeric char %c", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// analyzeEncFunc returns true is current byte matched in current mode,
|
||||||
|
// otherwise means you should use a bigger character set to check.
|
||||||
|
type analyzeEncFunc func(byte) bool
|
||||||
|
|
||||||
|
// analyzeEncodeModeFromRaw try to detect letter set of input data,
|
||||||
|
// so that encoder can determine which mode should be use.
|
||||||
|
// reference: https://en.wikipedia.org/wiki/QR_code
|
||||||
|
//
|
||||||
|
// case1: only numbers, use EncModeNumeric.
|
||||||
|
// case2: could not use EncModeNumeric, but you can find all of them in character mapping, use EncModeAlphanumeric.
|
||||||
|
// case3: could not use EncModeAlphanumeric, but you can find all of them in ISO-8859-1 character set, use EncModeByte.
|
||||||
|
// case4: could not use EncModeByte, use EncModeJP, no more choice.
|
||||||
|
//
|
||||||
|
func analyzeEncodeModeFromRaw(raw []byte) encMode {
|
||||||
|
analyzeFnMapping := map[encMode]analyzeEncFunc{
|
||||||
|
EncModeNumeric: analyzeNum,
|
||||||
|
EncModeAlphanumeric: analyzeAlphaNum,
|
||||||
|
EncModeByte: nil,
|
||||||
|
EncModeJP: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
f analyzeEncFunc
|
||||||
|
mode = EncModeNumeric
|
||||||
|
)
|
||||||
|
|
||||||
|
// loop to check each character in raw data,
|
||||||
|
// from low mode to higher while current mode could bearing the input data.
|
||||||
|
for _, byt := range raw {
|
||||||
|
reAnalyze:
|
||||||
|
if f = analyzeFnMapping[mode]; f == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// issue#28 @borislavone reports this bug.
|
||||||
|
// FIXED(@yeqown): next encMode analyzeVersionAuto func did not check the previous byte,
|
||||||
|
// add goto statement to reanalyze previous byte which can't be analyzed in last encMode.
|
||||||
|
if !f(byt) {
|
||||||
|
mode <<= 1
|
||||||
|
goto reAnalyze
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// analyzeNum is byt in num encMode
|
||||||
|
func analyzeNum(byt byte) bool {
|
||||||
|
return byt >= '0' && byt <= '9'
|
||||||
|
}
|
||||||
|
|
||||||
|
// analyzeAlphaNum is byt in alpha number
|
||||||
|
func analyzeAlphaNum(byt byte) bool {
|
||||||
|
if (byt >= '0' && byt <= '9') || (byt >= 'A' && byt <= 'Z') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switch byt {
|
||||||
|
case ' ', '$', '%', '*', '+', '-', '.', '/', ':':
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//// analyzeByte is byt in bytes.
|
||||||
|
//func analyzeByte(byt byte) qrbool {
|
||||||
|
// return false
|
||||||
|
//}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package qrcode
|
||||||
|
|
||||||
|
type EncodeOption interface {
|
||||||
|
apply(option *encodingOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultEncodingOption with EncMode = EncModeAuto, EcLevel = ErrorCorrectionQuart
|
||||||
|
func DefaultEncodingOption() *encodingOption {
|
||||||
|
return &encodingOption{
|
||||||
|
EncMode: EncModeAuto,
|
||||||
|
EcLevel: ErrorCorrectionQuart,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type encodingOption struct {
|
||||||
|
// Version of target QR code.
|
||||||
|
Version int
|
||||||
|
|
||||||
|
// EncMode specifies which encMode to use
|
||||||
|
EncMode encMode
|
||||||
|
|
||||||
|
// EcLevel specifies which ecLevel to use
|
||||||
|
EcLevel ecLevel
|
||||||
|
|
||||||
|
// PS: The version (which implicitly defines the byte capacity of the qrcode) is dynamically selected at runtime
|
||||||
|
}
|
||||||
|
|
||||||
|
type fnEncodingOption struct {
|
||||||
|
fn func(*encodingOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fnEncodingOption) apply(option *encodingOption) {
|
||||||
|
f.fn(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFnEncodingOption(fn func(*encodingOption)) fnEncodingOption {
|
||||||
|
return fnEncodingOption{fn: fn}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEncodingMode sets the encoding mode.
|
||||||
|
func WithEncodingMode(mode encMode) EncodeOption {
|
||||||
|
return newFnEncodingOption(func(option *encodingOption) {
|
||||||
|
if name := getEncModeName(mode); name == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
option.EncMode = mode
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithErrorCorrectionLevel sets the error correction level.
|
||||||
|
func WithErrorCorrectionLevel(ecLevel ecLevel) EncodeOption {
|
||||||
|
return newFnEncodingOption(func(option *encodingOption) {
|
||||||
|
if ecLevel < ErrorCorrectionLow || ecLevel > ErrorCorrectionHighest {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
option.EcLevel = ecLevel
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithVersion sets the version of target QR code.
|
||||||
|
func WithVersion(version int) EncodeOption {
|
||||||
|
return newFnEncodingOption(func(option *encodingOption) {
|
||||||
|
if version < 1 || version > 40 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
option.Version = version
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package qrcode
|
||||||
|
|
||||||
|
// kmp is variant of kmp algorithm to count the pattern been in
|
||||||
|
// src slice.
|
||||||
|
// DONE(@yeqown): implement this in generic way.
|
||||||
|
func kmp[v comparable](src, pattern []v, next []int) (count int) {
|
||||||
|
if next == nil {
|
||||||
|
next = kmpGetNext(pattern)
|
||||||
|
}
|
||||||
|
slen := len(src)
|
||||||
|
plen := len(pattern)
|
||||||
|
i := 0 // cursor of src
|
||||||
|
j := 0 // cursor of pattern
|
||||||
|
|
||||||
|
loop:
|
||||||
|
for i < slen && j < plen {
|
||||||
|
if j == -1 || src[i] == pattern[j] {
|
||||||
|
i++
|
||||||
|
j++
|
||||||
|
} else {
|
||||||
|
j = next[j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if j == plen {
|
||||||
|
if i-j >= 0 {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset cursor to count duplicate pattern.
|
||||||
|
// such as: "aaaa" and "aa", we want 3 rather than 2.
|
||||||
|
i -= plen - 1
|
||||||
|
j = 0
|
||||||
|
|
||||||
|
goto loop
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func kmpGetNext[v comparable](pattern []v) []int {
|
||||||
|
fail := make([]int, len(pattern))
|
||||||
|
fail[0] = -1
|
||||||
|
|
||||||
|
j := 0
|
||||||
|
k := -1
|
||||||
|
|
||||||
|
for j < len(pattern)-1 {
|
||||||
|
if k == -1 || pattern[j] == pattern[k] {
|
||||||
|
k++
|
||||||
|
j++
|
||||||
|
fail[j] = k
|
||||||
|
} else {
|
||||||
|
k = fail[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fail
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
package qrcode
|
||||||
|
|
||||||
|
// maskPatternModulo ...
|
||||||
|
// mask Pattern ref to: https://www.thonky.com/qr-code-tutorial/mask-patterns
|
||||||
|
type maskPatternModulo uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
// modulo0 (x+y) mod 2 == 0
|
||||||
|
modulo0 maskPatternModulo = iota
|
||||||
|
// modulo1 (x) mod 2 == 0
|
||||||
|
modulo1
|
||||||
|
// modulo2 (y) mod 3 == 0
|
||||||
|
modulo2
|
||||||
|
// modulo3 (x+y) mod 3 == 0
|
||||||
|
modulo3
|
||||||
|
// modulo4 (floor (x/ 2) + floor (y/ 3) mod 2 == 0
|
||||||
|
modulo4
|
||||||
|
// modulo5 (x * y) mod 2) + (x * y) mod 3) == 0
|
||||||
|
modulo5
|
||||||
|
// modulo6 (x * y) mod 2) + (x * y) mod 3) mod 2 == 0
|
||||||
|
modulo6
|
||||||
|
// modulo7 (x + y) mod 2) + (x * y) mod 3) mod 2 == 0
|
||||||
|
modulo7
|
||||||
|
)
|
||||||
|
|
||||||
|
type mask struct {
|
||||||
|
mat *Matrix // matrix
|
||||||
|
mode maskPatternModulo // mode
|
||||||
|
moduloFn moduloFunc // moduloFn masking function
|
||||||
|
}
|
||||||
|
|
||||||
|
// newMask ...
|
||||||
|
func newMask(mat *Matrix, mode maskPatternModulo) *mask {
|
||||||
|
m := &mask{
|
||||||
|
mat: mat.Copy(),
|
||||||
|
mode: mode,
|
||||||
|
moduloFn: getModuloFunc(mode),
|
||||||
|
}
|
||||||
|
m.masking()
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// moduloFunc to define what's modulo func
|
||||||
|
type moduloFunc func(int, int) bool
|
||||||
|
|
||||||
|
func getModuloFunc(mode maskPatternModulo) (f moduloFunc) {
|
||||||
|
f = nil
|
||||||
|
switch mode {
|
||||||
|
case modulo0:
|
||||||
|
f = modulo0Func
|
||||||
|
case modulo1:
|
||||||
|
f = modulo1Func
|
||||||
|
case modulo2:
|
||||||
|
f = modulo2Func
|
||||||
|
case modulo3:
|
||||||
|
f = modulo3Func
|
||||||
|
case modulo4:
|
||||||
|
f = modulo4Func
|
||||||
|
case modulo5:
|
||||||
|
f = modulo5Func
|
||||||
|
case modulo6:
|
||||||
|
f = modulo6Func
|
||||||
|
case modulo7:
|
||||||
|
f = modulo7Func
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// init generate maks by mode
|
||||||
|
func (m *mask) masking() {
|
||||||
|
moduloFn := m.moduloFn
|
||||||
|
if moduloFn == nil {
|
||||||
|
panic("impossible panic, contact maintainer plz")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mat.iter(IterDirection_COLUMN, func(x, y int, s qrvalue) {
|
||||||
|
// skip the function modules
|
||||||
|
if v, _ := m.mat.at(x, y); v.qrtype() != QRType_INIT {
|
||||||
|
_ = m.mat.set(x, y, QRValue_INIT_V0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if moduloFn(x, y) {
|
||||||
|
_ = m.mat.set(x, y, QRValue_DATA_V1)
|
||||||
|
} else {
|
||||||
|
_ = m.mat.set(x, y, QRValue_DATA_V0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// modulo0Func for maskPattern function
|
||||||
|
// modulo0 (x+y) mod 2 == 0
|
||||||
|
func modulo0Func(x, y int) bool {
|
||||||
|
return (x+y)%2 == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// modulo1Func for maskPattern function
|
||||||
|
// modulo1 (y) mod 2 == 0
|
||||||
|
func modulo1Func(x, y int) bool {
|
||||||
|
return y%2 == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// modulo2Func for maskPattern function
|
||||||
|
// modulo2 (x) mod 3 == 0
|
||||||
|
func modulo2Func(x, y int) bool {
|
||||||
|
return x%3 == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// modulo3Func for maskPattern function
|
||||||
|
// modulo3 (x+y) mod 3 == 0
|
||||||
|
func modulo3Func(x, y int) bool {
|
||||||
|
return (x+y)%3 == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// modulo4Func for maskPattern function
|
||||||
|
// modulo4 (floor (x/ 2) + floor (y/ 3) mod 2 == 0
|
||||||
|
func modulo4Func(x, y int) bool {
|
||||||
|
return (x/3+y/2)%2 == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// modulo5Func for maskPattern function
|
||||||
|
// modulo5 (x * y) mod 2 + (x * y) mod 3 == 0
|
||||||
|
func modulo5Func(x, y int) bool {
|
||||||
|
return (x*y)%2+(x*y)%3 == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// modulo6Func for maskPattern function
|
||||||
|
// modulo6 (x * y) mod 2) + (x * y) mod 3) mod 2 == 0
|
||||||
|
func modulo6Func(x, y int) bool {
|
||||||
|
return ((x*y)%2+(x*y)%3)%2 == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// modulo7Func for maskPattern function
|
||||||
|
// modulo7 (x + y) mod 2) + (x * y) mod 3) mod 2 == 0
|
||||||
|
func modulo7Func(x, y int) bool {
|
||||||
|
return ((x+y)%2+(x*y)%3)%2 == 0
|
||||||
|
}
|
|
@ -0,0 +1,172 @@
|
||||||
|
package qrcode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// evaluation calculate a score after masking matrix.
|
||||||
|
//
|
||||||
|
// reference:
|
||||||
|
// - https://www.thonky.com/qr-code-tutorial/data-masking#Determining-the-Best-Mask
|
||||||
|
func evaluation(mat *Matrix) (score int) {
|
||||||
|
debugLogf("calculate maskScore starting")
|
||||||
|
|
||||||
|
score1 := rule1(mat)
|
||||||
|
score2 := rule2(mat)
|
||||||
|
score3 := rule3(mat)
|
||||||
|
score4 := rule4(mat)
|
||||||
|
score = score1 + score2 + score3 + score4
|
||||||
|
debugLogf("maskScore: rule1=%d, rule2=%d, rule3=%d, rule4=%d", score1, score2, score3, score4)
|
||||||
|
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
|
// check each row one-by-one. If there are five consecutive modules of the same color,
|
||||||
|
// add 3 to the penalty. If there are more modules of the same color after the first five,
|
||||||
|
// add 1 for each additional module of the same color. Afterward, check each column one-by-one,
|
||||||
|
// checking for the same condition. Add the horizontal and vertical total to obtain penalty score
|
||||||
|
func rule1(mat *Matrix) (score int) {
|
||||||
|
// prerequisites:
|
||||||
|
// mat.Width() == mat.Height()
|
||||||
|
if mat.Width() != mat.Height() {
|
||||||
|
debugLogf("matrix width != height, skip rule1")
|
||||||
|
return math.MaxInt32
|
||||||
|
}
|
||||||
|
|
||||||
|
dimension := mat.Width()
|
||||||
|
scoreLine := func(arr []qrvalue) int {
|
||||||
|
lScore, cnt, cur := 0, 0, QRValue_INIT_V0
|
||||||
|
for _, v := range arr {
|
||||||
|
if !samestate(v, cur) {
|
||||||
|
cur = v
|
||||||
|
cnt = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cnt++
|
||||||
|
if cnt == 5 {
|
||||||
|
lScore += 3
|
||||||
|
} else if cnt > 5 {
|
||||||
|
lScore++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lScore
|
||||||
|
}
|
||||||
|
|
||||||
|
for cur := 0; cur < dimension; cur++ {
|
||||||
|
row := mat.Row(cur)
|
||||||
|
col := mat.Col(cur)
|
||||||
|
score += scoreLine(row)
|
||||||
|
score += scoreLine(col)
|
||||||
|
}
|
||||||
|
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
|
// rule2
|
||||||
|
// look for areas of the same color that are at least 2x2 modules or larger.
|
||||||
|
// The QR code specification says that for a solid-color block of size m × n,
|
||||||
|
// the penalty score is 3 × (m - 1) × (n - 1).
|
||||||
|
func rule2(mat *Matrix) int {
|
||||||
|
var (
|
||||||
|
score int
|
||||||
|
s0, s1, s2, s3 qrvalue
|
||||||
|
)
|
||||||
|
for x := 0; x < mat.Width()-1; x++ {
|
||||||
|
for y := 0; y < mat.Height()-1; y++ {
|
||||||
|
s0, _ = mat.at(x, y)
|
||||||
|
s1, _ = mat.at(x+1, y)
|
||||||
|
s2, _ = mat.at(x, y+1)
|
||||||
|
s3, _ = mat.at(x+1, y+1)
|
||||||
|
|
||||||
|
if s0 == s1 && s2 == s3 && s1 == s2 {
|
||||||
|
score += 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
|
// rule3 calculate punishment score in rule3, find pattern in QR Code matrix.
|
||||||
|
// Looks for patterns of dark-light-dark-dark-dark-light-dark that have four
|
||||||
|
// light modules on either side. In other words, it looks for any of the
|
||||||
|
// following two patterns: 1011101 0000 or 0000 1011101.
|
||||||
|
//
|
||||||
|
// Each time this pattern is found, add 40 to the penalty score.
|
||||||
|
func rule3(mat *Matrix) (score int) {
|
||||||
|
var (
|
||||||
|
pattern1 = binaryToQRValueSlice("1011101 0000")
|
||||||
|
pattern2 = binaryToQRValueSlice("0000 1011101")
|
||||||
|
pattern1Next = kmpGetNext(pattern1)
|
||||||
|
pattern2Next = kmpGetNext(pattern2)
|
||||||
|
)
|
||||||
|
|
||||||
|
// prerequisites:
|
||||||
|
//
|
||||||
|
// mat.Width() == mat.Height()
|
||||||
|
if mat.Width() != mat.Height() {
|
||||||
|
debugLogf("rule3 got matrix but not matched prerequisites")
|
||||||
|
return math.MaxInt32
|
||||||
|
}
|
||||||
|
dimension := mat.Width()
|
||||||
|
|
||||||
|
for i := 0; i < dimension; i++ {
|
||||||
|
col := mat.Col(i)
|
||||||
|
row := mat.Row(i)
|
||||||
|
|
||||||
|
// DONE(@yeqown): statePattern1 and statePattern2 are fixed, so maybe kmpGetNext
|
||||||
|
// could cache result to speed up.
|
||||||
|
score += 40 * kmp(col, pattern1, pattern1Next)
|
||||||
|
score += 40 * kmp(col, pattern2, pattern2Next)
|
||||||
|
score += 40 * kmp(row, pattern1, pattern1Next)
|
||||||
|
score += 40 * kmp(row, pattern2, pattern2Next)
|
||||||
|
}
|
||||||
|
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
|
// rule4 is based on the ratio of light modules to dark modules:
|
||||||
|
//
|
||||||
|
// 1. Count the total number of modules in the matrix.
|
||||||
|
// 2. Count how many dark modules there are in the matrix.
|
||||||
|
// 3. Calculate the percent of modules in the matrix that are dark: (darkmodules / totalmodules) * 100
|
||||||
|
// 4. Determine the previous and next multiple of five of this percent.
|
||||||
|
// 5. Subtract 50 from each of these multiples of five and take the absolute qrbool of the result.
|
||||||
|
// 6. Divide each of these by five. For example, 10/5 = 2 and 5/5 = 1.
|
||||||
|
// 7. Finally, take the smallest of the two numbers and multiply it by 10.
|
||||||
|
//
|
||||||
|
func rule4(mat *Matrix) int {
|
||||||
|
// prerequisites:
|
||||||
|
//
|
||||||
|
// mat.Width() == mat.Height()
|
||||||
|
if mat.Width() != mat.Height() {
|
||||||
|
debugLogf("rule4 got matrix but not matched prerequisites")
|
||||||
|
return math.MaxInt32
|
||||||
|
}
|
||||||
|
|
||||||
|
dimension := mat.Width()
|
||||||
|
dark, total := 0, dimension*dimension
|
||||||
|
for i := 0; i < dimension; i++ {
|
||||||
|
col := mat.Col(i)
|
||||||
|
|
||||||
|
// count dark modules
|
||||||
|
for j := 0; j < dimension; j++ {
|
||||||
|
if samestate(col[j], QRValue_DATA_V1) {
|
||||||
|
dark++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ratio := (dark * 100) / total // in range [0, 100]
|
||||||
|
step := 0
|
||||||
|
if ratio%5 == 0 {
|
||||||
|
step = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
previous := abs((ratio/5-step)*5 - 50)
|
||||||
|
next := abs((ratio/5+1-step)*5 - 50)
|
||||||
|
|
||||||
|
return min(previous, next) / 5 * 10
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
package qrcode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrorOutRangeOfW x out of range of Width
|
||||||
|
ErrorOutRangeOfW = errors.New("out of range of width")
|
||||||
|
|
||||||
|
// ErrorOutRangeOfH y out of range of Height
|
||||||
|
ErrorOutRangeOfH = errors.New("out of range of height")
|
||||||
|
)
|
||||||
|
|
||||||
|
// newMatrix generate a matrix with map[][]qrbool
|
||||||
|
func newMatrix(width, height int) *Matrix {
|
||||||
|
mat := make([][]qrvalue, width)
|
||||||
|
for w := 0; w < width; w++ {
|
||||||
|
mat[w] = make([]qrvalue, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := &Matrix{
|
||||||
|
mat: mat,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.init()
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matrix is a matrix data type
|
||||||
|
// width:3 height: 4 for [3][4]int
|
||||||
|
type Matrix struct {
|
||||||
|
mat [][]qrvalue
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
}
|
||||||
|
|
||||||
|
// do some init work
|
||||||
|
func (m *Matrix) init() {
|
||||||
|
for w := 0; w < m.width; w++ {
|
||||||
|
for h := 0; h < m.height; h++ {
|
||||||
|
m.mat[w][h] = QRValue_INIT_V0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// print to stdout
|
||||||
|
func (m *Matrix) print() {
|
||||||
|
m.iter(IterDirection_ROW, func(x, y int, s qrvalue) {
|
||||||
|
fmt.Printf("%s ", s)
|
||||||
|
if (x + 1) == m.width {
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy matrix into a new Matrix
|
||||||
|
func (m *Matrix) Copy() *Matrix {
|
||||||
|
mat2 := make([][]qrvalue, m.width)
|
||||||
|
for w := 0; w < m.width; w++ {
|
||||||
|
mat2[w] = make([]qrvalue, m.height)
|
||||||
|
copy(mat2[w], m.mat[w])
|
||||||
|
}
|
||||||
|
|
||||||
|
m2 := &Matrix{
|
||||||
|
width: m.width,
|
||||||
|
height: m.height,
|
||||||
|
mat: mat2,
|
||||||
|
}
|
||||||
|
|
||||||
|
return m2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width ... width
|
||||||
|
func (m *Matrix) Width() int {
|
||||||
|
return m.width
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height ... height
|
||||||
|
func (m *Matrix) Height() int {
|
||||||
|
return m.height
|
||||||
|
}
|
||||||
|
|
||||||
|
// set [w][h] as true
|
||||||
|
func (m *Matrix) set(w, h int, c qrvalue) error {
|
||||||
|
if w >= m.width || w < 0 {
|
||||||
|
return ErrorOutRangeOfW
|
||||||
|
}
|
||||||
|
if h >= m.height || h < 0 {
|
||||||
|
return ErrorOutRangeOfH
|
||||||
|
}
|
||||||
|
m.mat[w][h] = c
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// at state qrvalue from matrix with position {x, y}
|
||||||
|
func (m *Matrix) at(w, h int) (qrvalue, error) {
|
||||||
|
if w >= m.width || w < 0 {
|
||||||
|
return QRValue_INIT_V0, ErrorOutRangeOfW
|
||||||
|
}
|
||||||
|
if h >= m.height || h < 0 {
|
||||||
|
return QRValue_INIT_V0, ErrorOutRangeOfH
|
||||||
|
}
|
||||||
|
return m.mat[w][h], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterDirection scan matrix direction
|
||||||
|
type iterDirection uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// IterDirection_ROW for row first
|
||||||
|
IterDirection_ROW iterDirection = iota + 1
|
||||||
|
|
||||||
|
// IterDirection_COLUMN for column first
|
||||||
|
IterDirection_COLUMN
|
||||||
|
)
|
||||||
|
|
||||||
|
// Iterate the Matrix with loop direction IterDirection_ROW major or IterDirection_COLUMN major.
|
||||||
|
// IterDirection_COLUMN is recommended.
|
||||||
|
func (m *Matrix) Iterate(direction iterDirection, fn func(x, y int, s QRValue)) {
|
||||||
|
m.iter(direction, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Matrix) iter(dir iterDirection, visitFn func(x int, y int, v qrvalue)) {
|
||||||
|
// row direction first
|
||||||
|
if dir == IterDirection_ROW {
|
||||||
|
for h := 0; h < m.height; h++ {
|
||||||
|
for w := 0; w < m.width; w++ {
|
||||||
|
visitFn(w, h, m.mat[w][h])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// column direction first
|
||||||
|
for w := 0; w < m.width; w++ {
|
||||||
|
for h := 0; h < m.height; h++ {
|
||||||
|
visitFn(w, h, m.mat[w][h])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row return a row of matrix, cur should be y dimension.
|
||||||
|
func (m *Matrix) Row(cur int) []qrvalue {
|
||||||
|
if cur >= m.height || cur < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
col := make([]qrvalue, m.height)
|
||||||
|
for w := 0; w < m.width; w++ {
|
||||||
|
col[w] = m.mat[w][cur]
|
||||||
|
}
|
||||||
|
return col
|
||||||
|
}
|
||||||
|
|
||||||
|
// Col return a slice of column, cur should be x dimension.
|
||||||
|
func (m *Matrix) Col(cur int) []qrvalue {
|
||||||
|
if cur >= m.width || cur < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.mat[cur]
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
package qrcode
|
||||||
|
|
||||||
|
type QRType = qrtype
|
||||||
|
|
||||||
|
// qrtype
|
||||||
|
type qrtype uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// QRType_INIT represents the initial block state of the matrix
|
||||||
|
QRType_INIT qrtype = 1 << 1
|
||||||
|
// QRType_DATA represents the data block state of the matrix
|
||||||
|
QRType_DATA qrtype = 2 << 1
|
||||||
|
// QRType_VERSION indicates the version block of matrix
|
||||||
|
QRType_VERSION qrtype = 3 << 1
|
||||||
|
// QRType_FORMAT indicates the format block of matrix
|
||||||
|
QRType_FORMAT qrtype = 4 << 1
|
||||||
|
// QRType_FINDER indicates the finder block of matrix
|
||||||
|
QRType_FINDER qrtype = 5 << 1
|
||||||
|
// QRType_DARK ...
|
||||||
|
QRType_DARK qrtype = 6 << 1
|
||||||
|
QRType_SPLITTER qrtype = 7 << 1
|
||||||
|
QRType_TIMING qrtype = 8 << 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s qrtype) String() string {
|
||||||
|
switch s {
|
||||||
|
case QRType_INIT:
|
||||||
|
return "I"
|
||||||
|
case QRType_DATA:
|
||||||
|
return "d"
|
||||||
|
case QRType_VERSION:
|
||||||
|
return "V"
|
||||||
|
case QRType_FORMAT:
|
||||||
|
return "f"
|
||||||
|
case QRType_FINDER:
|
||||||
|
return "F"
|
||||||
|
case QRType_DARK:
|
||||||
|
return "D"
|
||||||
|
case QRType_SPLITTER:
|
||||||
|
return "S"
|
||||||
|
case QRType_TIMING:
|
||||||
|
return "T"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
type QRValue = qrvalue
|
||||||
|
|
||||||
|
func (v QRValue) Type() qrtype {
|
||||||
|
return v.qrtype()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v QRValue) IsSet() bool {
|
||||||
|
return v.qrbool()
|
||||||
|
}
|
||||||
|
|
||||||
|
// qrvalue represents the value of the matrix, it is composed of the qrtype(7bits) and the value(1bits).
|
||||||
|
// such as: 0b0000,0011 (QRValue_DATA_V1) represents the qrtype is QRType_DATA and the value is 1.
|
||||||
|
type qrvalue uint8
|
||||||
|
|
||||||
|
var (
|
||||||
|
// QRValue_INIT_V0 represents the value 0
|
||||||
|
QRValue_INIT_V0 = qrvalue(QRType_INIT | 0)
|
||||||
|
|
||||||
|
// QRValue_DATA_V0 represents the block has been set to false
|
||||||
|
QRValue_DATA_V0 = qrvalue(QRType_DATA | 0)
|
||||||
|
// QRValue_DATA_V1 represents the block has been set to TRUE
|
||||||
|
QRValue_DATA_V1 = qrvalue(QRType_DATA | 1)
|
||||||
|
|
||||||
|
// QRValue_VERSION_V0 represents the block has been set to false
|
||||||
|
QRValue_VERSION_V0 = qrvalue(QRType_VERSION | 0)
|
||||||
|
// QRValue_VERSION_V1 represents the block has been set to TRUE
|
||||||
|
QRValue_VERSION_V1 = qrvalue(QRType_VERSION | 1)
|
||||||
|
|
||||||
|
// QRValue_FORMAT_V0 represents the block has been set to false
|
||||||
|
QRValue_FORMAT_V0 = qrvalue(QRType_FORMAT | 0)
|
||||||
|
// QRValue_FORMAT_V1 represents the block has been set to TRUE
|
||||||
|
QRValue_FORMAT_V1 = qrvalue(QRType_FORMAT | 1)
|
||||||
|
|
||||||
|
// QRValue_FINDER_V0 represents the block has been set to false
|
||||||
|
QRValue_FINDER_V0 = qrvalue(QRType_FINDER | 0)
|
||||||
|
// QRValue_FINDER_V1 represents the block has been set to TRUE
|
||||||
|
QRValue_FINDER_V1 = qrvalue(QRType_FINDER | 1)
|
||||||
|
|
||||||
|
// QRValue_DARK_V0 represents the block has been set to false
|
||||||
|
QRValue_DARK_V0 = qrvalue(QRType_DARK | 0)
|
||||||
|
// QRValue_DARK_V1 represents the block has been set to TRUE
|
||||||
|
QRValue_DARK_V1 = qrvalue(QRType_DARK | 1)
|
||||||
|
|
||||||
|
// QRValue_SPLITTER_V0 represents the block has been set to false
|
||||||
|
QRValue_SPLITTER_V0 = qrvalue(QRType_SPLITTER | 0)
|
||||||
|
// QRValue_SPLITTER_V1 represents the block has been set to TRUE
|
||||||
|
QRValue_SPLITTER_V1 = qrvalue(QRType_SPLITTER | 1)
|
||||||
|
|
||||||
|
// QRValue_TIMING_V0 represents the block has been set to false
|
||||||
|
QRValue_TIMING_V0 = qrvalue(QRType_TIMING | 0)
|
||||||
|
// QRValue_TIMING_V1 represents the block has been set to TRUE
|
||||||
|
QRValue_TIMING_V1 = qrvalue(QRType_TIMING | 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v qrvalue) qrtype() qrtype {
|
||||||
|
return qrtype(v & 0xfe)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v qrvalue) qrbool() bool {
|
||||||
|
return v&0x01 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v qrvalue) String() string {
|
||||||
|
t := v.qrtype()
|
||||||
|
if v.qrbool() {
|
||||||
|
return t.String() + "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.String() + "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v qrvalue) xor(v2 qrvalue) qrvalue {
|
||||||
|
if v != v2 {
|
||||||
|
return QRValue_DATA_V1
|
||||||
|
}
|
||||||
|
|
||||||
|
return QRValue_DATA_V0
|
||||||
|
}
|
|
@ -0,0 +1,738 @@
|
||||||
|
package qrcode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/yeqown/reedsolomon"
|
||||||
|
"github.com/yeqown/reedsolomon/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New generate a QRCode struct to create
|
||||||
|
func New(text string) (*QRCode, error) {
|
||||||
|
dst := DefaultEncodingOption()
|
||||||
|
return build(text, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWith generate a QRCode struct with
|
||||||
|
// specified `ver`(QR version) and `ecLv`(Error Correction level)
|
||||||
|
func NewWith(text string, opts ...EncodeOption) (*QRCode, error) {
|
||||||
|
dst := DefaultEncodingOption()
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt.apply(dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
return build(text, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func build(text string, option *encodingOption) (*QRCode, error) {
|
||||||
|
qrc := &QRCode{
|
||||||
|
sourceText: text,
|
||||||
|
sourceRawBytes: []byte(text),
|
||||||
|
dataBSet: nil,
|
||||||
|
mat: nil,
|
||||||
|
ecBSet: nil,
|
||||||
|
v: version{},
|
||||||
|
encodingOption: option,
|
||||||
|
encoder: nil,
|
||||||
|
}
|
||||||
|
// initialize QRCode instance
|
||||||
|
if err := qrc.init(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
qrc.masking()
|
||||||
|
|
||||||
|
return qrc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QRCode contains fields to generate QRCode matrix, outputImageOptions to Draw image,
|
||||||
|
// etc.
|
||||||
|
type QRCode struct {
|
||||||
|
sourceText string // sourceText input text
|
||||||
|
sourceRawBytes []byte // raw Data to transfer
|
||||||
|
|
||||||
|
dataBSet *binary.Binary // final data bit stream of encode data
|
||||||
|
mat *Matrix // matrix grid to store final bitmap
|
||||||
|
ecBSet *binary.Binary // final error correction bitset
|
||||||
|
|
||||||
|
encodingOption *encodingOption
|
||||||
|
encoder *encoder // encoder ptr to call its methods ~
|
||||||
|
v version // indicate the QR version to encode.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QRCode) Save(w Writer) error {
|
||||||
|
if w == nil {
|
||||||
|
w = nonWriter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
log.Printf("[WARNNING] [go-qrcode] close writer failed: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return w.Write(*q.mat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QRCode) Dimension() int {
|
||||||
|
if q.mat == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.mat.Width()
|
||||||
|
}
|
||||||
|
|
||||||
|
// init fill QRCode instance from settings and sourceText.
|
||||||
|
func (q *QRCode) init() (err error) {
|
||||||
|
// choose encode mode (num, alpha num, byte, Japanese)
|
||||||
|
if q.encodingOption.EncMode == EncModeAuto {
|
||||||
|
q.encodingOption.EncMode = analyzeEncodeModeFromRaw(q.sourceRawBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// choose version
|
||||||
|
if _, err = q.calcVersion(); err != nil {
|
||||||
|
return fmt.Errorf("init: calc version failed: %v", err)
|
||||||
|
}
|
||||||
|
q.mat = newMatrix(q.v.Dimension(), q.v.Dimension())
|
||||||
|
_ = q.applyEncoder()
|
||||||
|
|
||||||
|
var (
|
||||||
|
dataBlocks []dataBlock // data encoding blocks
|
||||||
|
ecBlocks []ecBlock // error correction blocks
|
||||||
|
)
|
||||||
|
|
||||||
|
// data encoding, and be split into blocks
|
||||||
|
if dataBlocks, err = q.dataEncoding(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate er bitsets, and also be split into blocks
|
||||||
|
if ecBlocks, err = q.errorCorrectionEncoding(dataBlocks); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// arrange data blocks and EC blocks
|
||||||
|
q.arrangeBits(dataBlocks, ecBlocks)
|
||||||
|
// append ec bits after data bits
|
||||||
|
q.dataBSet.Append(q.ecBSet)
|
||||||
|
// append remainder bits
|
||||||
|
q.dataBSet.AppendNumBools(q.v.RemainderBits, false)
|
||||||
|
// initial the 2d matrix
|
||||||
|
q.prefillMatrix()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// calcVersion
|
||||||
|
func (q *QRCode) calcVersion() (ver *version, err error) {
|
||||||
|
var needAnalyze = true
|
||||||
|
|
||||||
|
opt := q.encodingOption
|
||||||
|
if opt.Version >= 1 && opt.Version <= 40 &&
|
||||||
|
opt.EcLevel >= ErrorCorrectionLow && opt.EcLevel <= ErrorCorrectionHighest {
|
||||||
|
// only version and EC level are specified, can skip analyzeVersionAuto
|
||||||
|
needAnalyze = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// automatically parse version
|
||||||
|
if needAnalyze {
|
||||||
|
// analyzeVersion the input data to choose to adapt version
|
||||||
|
analyzed, err2 := analyzeVersion(q.sourceRawBytes, opt.EcLevel, opt.EncMode)
|
||||||
|
if err2 != nil {
|
||||||
|
err = fmt.Errorf("calcVersion: analyzeVersionAuto failed: %v", err2)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
opt.Version = analyzed.Ver
|
||||||
|
}
|
||||||
|
|
||||||
|
q.v = loadVersion(opt.Version, opt.EcLevel)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyEncoder
|
||||||
|
func (q *QRCode) applyEncoder() error {
|
||||||
|
q.encoder = newEncoder(q.encodingOption.EncMode, q.encodingOption.EcLevel, q.v)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dataEncoding ref to:
|
||||||
|
// https://www.thonky.com/qr-code-tutorial/data-encoding
|
||||||
|
func (q *QRCode) dataEncoding() (blocks []dataBlock, err error) {
|
||||||
|
var (
|
||||||
|
bset *binary.Binary
|
||||||
|
)
|
||||||
|
bset, err = q.encoder.Encode(q.sourceRawBytes)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("could not encode data: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks = make([]dataBlock, q.v.TotalNumBlocks())
|
||||||
|
|
||||||
|
// split bitset into data Block
|
||||||
|
start, end, blockID := 0, 0, 0
|
||||||
|
for _, g := range q.v.Groups {
|
||||||
|
for j := 0; j < g.NumBlocks; j++ {
|
||||||
|
start = end
|
||||||
|
end = start + g.NumDataCodewords*8
|
||||||
|
|
||||||
|
blocks[blockID].Data, err = bset.Subset(start, end)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
blocks[blockID].StartOffset = end - start
|
||||||
|
blocks[blockID].NumECBlock = g.ECBlockwordsPerBlock
|
||||||
|
|
||||||
|
blockID++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// dataBlock ...
|
||||||
|
type dataBlock struct {
|
||||||
|
Data *binary.Binary
|
||||||
|
StartOffset int // length
|
||||||
|
NumECBlock int // error correction codewords num per data block
|
||||||
|
}
|
||||||
|
|
||||||
|
// ecBlock ...
|
||||||
|
type ecBlock struct {
|
||||||
|
Data *binary.Binary
|
||||||
|
// StartOffset int // length
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorCorrectionEncoding ref to:
|
||||||
|
// https://www.thonky.com/qr-code-tutorial /error-correction-coding
|
||||||
|
func (q *QRCode) errorCorrectionEncoding(dataBlocks []dataBlock) (blocks []ecBlock, err error) {
|
||||||
|
// start, end, blockID := 0, 0, 0
|
||||||
|
blocks = make([]ecBlock, q.v.TotalNumBlocks())
|
||||||
|
for idx, b := range dataBlocks {
|
||||||
|
debugLogf("numOfECBlock: %d", b.NumECBlock)
|
||||||
|
bset := reedsolomon.Encode(b.Data, b.NumECBlock)
|
||||||
|
blocks[idx].Data, err = bset.Subset(b.StartOffset, bset.Len())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// blocks[idx].StartOffset = b.StartOffset
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// arrangeBits ... and save into dataBSet
|
||||||
|
func (q *QRCode) arrangeBits(dataBlocks []dataBlock, ecBlocks []ecBlock) {
|
||||||
|
if debugEnabled() {
|
||||||
|
log.Println("arrangeBits called, before")
|
||||||
|
for i := 0; i < len(ecBlocks); i++ {
|
||||||
|
debugLogf("ec block_%d: %v", i, ecBlocks[i])
|
||||||
|
}
|
||||||
|
for i := 0; i < len(dataBlocks); i++ {
|
||||||
|
debugLogf("data block_%d: %v", i, dataBlocks[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// arrange data blocks
|
||||||
|
var (
|
||||||
|
overflowCnt = 0
|
||||||
|
endFlag = false
|
||||||
|
curIdx = 0
|
||||||
|
start, end int
|
||||||
|
)
|
||||||
|
|
||||||
|
// check if bitsets initialized, or initial them
|
||||||
|
if q.dataBSet == nil {
|
||||||
|
q.dataBSet = binary.New()
|
||||||
|
}
|
||||||
|
if q.ecBSet == nil {
|
||||||
|
q.ecBSet = binary.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
for !endFlag {
|
||||||
|
for _, block := range dataBlocks {
|
||||||
|
start = curIdx * 8
|
||||||
|
end = start + 8
|
||||||
|
if start >= block.Data.Len() {
|
||||||
|
overflowCnt++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
subBin, err := block.Data.Subset(start, end)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
q.dataBSet.Append(subBin)
|
||||||
|
debugLogf("arrange data blocks info: start: %d, end: %d, len: %d, overflowCnt: %d, curIdx: %d",
|
||||||
|
start, end, block.Data.Len(), overflowCnt, curIdx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
curIdx++
|
||||||
|
// loop finish check
|
||||||
|
if overflowCnt >= len(dataBlocks) {
|
||||||
|
endFlag = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// arrange ec blocks and reinitialize
|
||||||
|
endFlag = false
|
||||||
|
overflowCnt = 0
|
||||||
|
curIdx = 0
|
||||||
|
|
||||||
|
for !endFlag {
|
||||||
|
for _, block := range ecBlocks {
|
||||||
|
start = curIdx * 8
|
||||||
|
end = start + 8
|
||||||
|
|
||||||
|
if start >= block.Data.Len() {
|
||||||
|
overflowCnt++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
subBin, err := block.Data.Subset(start, end)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
q.ecBSet.Append(subBin)
|
||||||
|
}
|
||||||
|
curIdx++
|
||||||
|
// loop finish check
|
||||||
|
if overflowCnt >= len(ecBlocks) {
|
||||||
|
endFlag = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLogf("arrangeBits called, after")
|
||||||
|
debugLogf("data bitsets: %s", q.dataBSet.String())
|
||||||
|
debugLogf("ec bitsets: %s", q.ecBSet.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefillMatrix with version info: ref to:
|
||||||
|
// http://www.thonky.com/qr-code-tutorial/module-placement-matrix
|
||||||
|
func (q *QRCode) prefillMatrix() {
|
||||||
|
dimension := q.v.Dimension()
|
||||||
|
if q.mat == nil {
|
||||||
|
q.mat = newMatrix(dimension, dimension)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add finder left-top
|
||||||
|
addFinder(q.mat, 0, 0)
|
||||||
|
addSplitter(q.mat, 7, 7, dimension)
|
||||||
|
debugLogf("finish left-top finder")
|
||||||
|
// add finder right-top
|
||||||
|
addFinder(q.mat, dimension-7, 0)
|
||||||
|
addSplitter(q.mat, dimension-8, 7, dimension)
|
||||||
|
debugLogf("finish right-top finder")
|
||||||
|
// add finder left-bottom
|
||||||
|
addFinder(q.mat, 0, dimension-7)
|
||||||
|
addSplitter(q.mat, 7, dimension-8, dimension)
|
||||||
|
debugLogf("finish left-bottom finder")
|
||||||
|
|
||||||
|
// only version-1 QR code has no alignment module
|
||||||
|
if q.v.Ver > 1 {
|
||||||
|
// add align-mode related to version cfg
|
||||||
|
for _, loc := range loadAlignmentPatternLoc(q.v.Ver) {
|
||||||
|
addAlignment(q.mat, loc.X, loc.Y)
|
||||||
|
}
|
||||||
|
debugLogf("finish align")
|
||||||
|
}
|
||||||
|
// add timing line
|
||||||
|
addTimingLine(q.mat, dimension)
|
||||||
|
// add darkBlock always be position (4*ver+9, 8)
|
||||||
|
addDarkBlock(q.mat, 8, 4*q.v.Ver+9)
|
||||||
|
// reserveFormatBlock for version and format info
|
||||||
|
reserveFormatBlock(q.mat, dimension)
|
||||||
|
|
||||||
|
// reserveVersionBlock for version over 7
|
||||||
|
// only version 7 and larger version should add version info
|
||||||
|
if q.v.Ver >= 7 {
|
||||||
|
reserveVersionBlock(q.mat, dimension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add finder module
|
||||||
|
func addFinder(m *Matrix, top, left int) {
|
||||||
|
// black outer
|
||||||
|
x, y := top, left
|
||||||
|
for i := 0; i < 24; i++ {
|
||||||
|
_ = m.set(x, y, QRValue_FINDER_V1)
|
||||||
|
if i < 6 {
|
||||||
|
x = x + 1
|
||||||
|
} else if i < 12 {
|
||||||
|
y = y + 1
|
||||||
|
} else if i < 18 {
|
||||||
|
x = x - 1
|
||||||
|
} else {
|
||||||
|
y = y - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// white inner
|
||||||
|
x, y = top+1, left+1
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
_ = m.set(x, y, QRValue_FINDER_V0)
|
||||||
|
if i < 4 {
|
||||||
|
x = x + 1
|
||||||
|
} else if i < 8 {
|
||||||
|
y = y + 1
|
||||||
|
} else if i < 12 {
|
||||||
|
x = x - 1
|
||||||
|
} else {
|
||||||
|
y = y - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// black inner
|
||||||
|
for x = left + 2; x < left+5; x++ {
|
||||||
|
for y = top + 2; y < top+5; y++ {
|
||||||
|
_ = m.set(x, y, QRValue_FINDER_V1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add splitter module
|
||||||
|
func addSplitter(m *Matrix, x, y, dimension int) {
|
||||||
|
// top-left
|
||||||
|
if x == 7 && y == 7 {
|
||||||
|
for pos := 0; pos < 8; pos++ {
|
||||||
|
_ = m.set(x, pos, QRValue_SPLITTER_V0)
|
||||||
|
_ = m.set(pos, y, QRValue_SPLITTER_V0)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// top-right
|
||||||
|
if x == dimension-8 && y == 7 {
|
||||||
|
for pos := 0; pos < 8; pos++ {
|
||||||
|
_ = m.set(x, y-pos, QRValue_SPLITTER_V0)
|
||||||
|
_ = m.set(x+pos, y, QRValue_SPLITTER_V0)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// bottom-left
|
||||||
|
if x == 7 && y == dimension-8 {
|
||||||
|
for pos := 0; pos < 8; pos++ {
|
||||||
|
_ = m.set(x, y+pos, QRValue_SPLITTER_V0)
|
||||||
|
_ = m.set(x-pos, y, QRValue_SPLITTER_V0)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// add matrix align module
|
||||||
|
func addAlignment(m *Matrix, centerX, centerY int) {
|
||||||
|
_ = m.set(centerX, centerY, QRValue_DATA_V1)
|
||||||
|
// black
|
||||||
|
x, y := centerX-2, centerY-2
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
_ = m.set(x, y, QRValue_DATA_V1)
|
||||||
|
if i < 4 {
|
||||||
|
x = x + 1
|
||||||
|
} else if i < 8 {
|
||||||
|
y = y + 1
|
||||||
|
} else if i < 12 {
|
||||||
|
x = x - 1
|
||||||
|
} else {
|
||||||
|
y = y - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// white
|
||||||
|
x, y = centerX-1, centerY-1
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
_ = m.set(x, y, QRValue_DATA_V0)
|
||||||
|
if i < 2 {
|
||||||
|
x = x + 1
|
||||||
|
} else if i < 4 {
|
||||||
|
y = y + 1
|
||||||
|
} else if i < 6 {
|
||||||
|
x = x - 1
|
||||||
|
} else {
|
||||||
|
y = y - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addTimingLine ...
|
||||||
|
func addTimingLine(m *Matrix, dimension int) {
|
||||||
|
for pos := 8; pos < dimension-8; pos++ {
|
||||||
|
if pos%2 == 0 {
|
||||||
|
_ = m.set(6, pos, QRValue_TIMING_V1)
|
||||||
|
_ = m.set(pos, 6, QRValue_TIMING_V1)
|
||||||
|
} else {
|
||||||
|
_ = m.set(6, pos, QRValue_TIMING_V0)
|
||||||
|
_ = m.set(pos, 6, QRValue_TIMING_V0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addDarkBlock ...
|
||||||
|
func addDarkBlock(m *Matrix, x, y int) {
|
||||||
|
_ = m.set(x, y, QRValue_DARK_V1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reserveFormatBlock maintain the position in matrix for format info
|
||||||
|
func reserveFormatBlock(m *Matrix, dimension int) {
|
||||||
|
for pos := 1; pos < 9; pos++ {
|
||||||
|
// skip timing line
|
||||||
|
if pos == 6 {
|
||||||
|
_ = m.set(8, dimension-pos, QRValue_FORMAT_V0)
|
||||||
|
_ = m.set(dimension-pos, 8, QRValue_FORMAT_V0)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// skip dark module
|
||||||
|
if pos == 8 {
|
||||||
|
_ = m.set(8, pos, QRValue_FORMAT_V0) // top-left-column
|
||||||
|
_ = m.set(pos, 8, QRValue_FORMAT_V0) // top-left-row
|
||||||
|
_ = m.set(dimension-pos, 8, QRValue_FORMAT_V0) // top-right-row
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_ = m.set(8, pos, QRValue_FORMAT_V0) // top-left-column
|
||||||
|
_ = m.set(pos, 8, QRValue_FORMAT_V0) // top-left-row
|
||||||
|
_ = m.set(dimension-pos, 8, QRValue_FORMAT_V0) // top-right-row
|
||||||
|
_ = m.set(8, dimension-pos, QRValue_FORMAT_V0) // bottom-left-column
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix(@yeqown): b4b5ae3 reduced two format reversed blocks on top-left-column and top-left-row.
|
||||||
|
_ = m.set(0, 8, QRValue_FORMAT_V0)
|
||||||
|
_ = m.set(8, 0, QRValue_FORMAT_V0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reserveVersionBlock maintain the position in matrix for version info
|
||||||
|
func reserveVersionBlock(m *Matrix, dimension int) {
|
||||||
|
// 3x6=18 cells
|
||||||
|
for i := 1; i <= 3; i++ {
|
||||||
|
for pos := 0; pos < 6; pos++ {
|
||||||
|
_ = m.set(dimension-8-i, pos, QRValue_VERSION_V0)
|
||||||
|
_ = m.set(pos, dimension-8-i, QRValue_VERSION_V0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fillDataBinary fill q.dataBSet binary stream into q.mat.
|
||||||
|
// References:
|
||||||
|
// * http://www.thonky.com/qr-code-tutorial/module-placement-matrix#Place-the-Data-Bits
|
||||||
|
//
|
||||||
|
func (q *QRCode) fillDataBinary(m *Matrix, dimension int) {
|
||||||
|
var (
|
||||||
|
// x always move from right, left right loop (2 rows), y move upward, downward, upward loop
|
||||||
|
x, y = dimension - 1, dimension - 1
|
||||||
|
l = q.dataBSet.Len()
|
||||||
|
upForward = true
|
||||||
|
pos int
|
||||||
|
)
|
||||||
|
|
||||||
|
for i := 0; pos < l; i++ {
|
||||||
|
// debugLogf("fillDataBinary: dimension: %d, len: %d: pos: %d", dimension, l, pos)
|
||||||
|
set := QRValue_DATA_V0
|
||||||
|
if q.dataBSet.At(pos) {
|
||||||
|
set = QRValue_DATA_V1
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := m.at(x, y)
|
||||||
|
if err != nil {
|
||||||
|
if err == ErrorOutRangeOfW {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == ErrorOutRangeOfH {
|
||||||
|
// turn around while y is out of range.
|
||||||
|
x = x - 2
|
||||||
|
switch upForward {
|
||||||
|
case true:
|
||||||
|
y = y + 1
|
||||||
|
default:
|
||||||
|
y = y - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if x == 7 || x == 6 {
|
||||||
|
x = x - 1
|
||||||
|
}
|
||||||
|
upForward = !upForward
|
||||||
|
state, err = m.at(x, y) // renew state qrbool after turn around writing direction.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// data bit should only be set into un-set block in matrix.
|
||||||
|
if state.qrtype() == QRType_INIT {
|
||||||
|
_ = m.set(x, y, set)
|
||||||
|
pos++
|
||||||
|
debugLogf("normal set turn forward: upForward: %v, x: %d, y: %d", upForward, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DO NOT CHANGE FOLLOWING CODE FOR NOW !!!
|
||||||
|
// change x, y
|
||||||
|
mod2 := i % 2
|
||||||
|
|
||||||
|
// in one 8bit block
|
||||||
|
if upForward {
|
||||||
|
if mod2 == 0 {
|
||||||
|
x = x - 1
|
||||||
|
} else {
|
||||||
|
y = y - 1
|
||||||
|
x = x + 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if mod2 == 0 {
|
||||||
|
x = x - 1
|
||||||
|
} else {
|
||||||
|
y = y + 1
|
||||||
|
x = x + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLogf("fillDone and x: %d, y: %d, pos: %d, total: %d", x, y, pos, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw from bitset to matrix.Matrix, calculate all mask modula score,
|
||||||
|
// then decide which mask to use according to the mask's score (the lowest one).
|
||||||
|
func (q *QRCode) masking() {
|
||||||
|
type maskScore struct {
|
||||||
|
Score int
|
||||||
|
Idx int
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
masks = make([]*mask, 8)
|
||||||
|
mats = make([]*Matrix, 8)
|
||||||
|
lowScore = math.MaxInt32
|
||||||
|
markMatsIdx int
|
||||||
|
scoreChan = make(chan maskScore, 8)
|
||||||
|
wg sync.WaitGroup
|
||||||
|
)
|
||||||
|
|
||||||
|
dimension := q.v.Dimension()
|
||||||
|
|
||||||
|
// fill bitset into matrix
|
||||||
|
cpy := q.mat.Copy()
|
||||||
|
q.fillDataBinary(cpy, dimension)
|
||||||
|
|
||||||
|
// init mask and mats
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
masks[i] = newMask(q.mat, maskPatternModulo(i))
|
||||||
|
mats[i] = cpy.Copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate 8 matrix with mask
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int) {
|
||||||
|
_ = debugDraw(fmt.Sprintf("draft/mats_%d.jpeg", i), *mats[i])
|
||||||
|
_ = debugDraw(fmt.Sprintf("draft/mask_%d.jpeg", i), *masks[i].mat)
|
||||||
|
|
||||||
|
// xor with mask
|
||||||
|
q.xorMask(mats[i], masks[i])
|
||||||
|
|
||||||
|
_ = debugDraw(fmt.Sprintf("draft/mats_mask_%d.jpeg", i), *mats[i])
|
||||||
|
|
||||||
|
// fill format info
|
||||||
|
q.fillFormatInfo(mats[i], maskPatternModulo(i), dimension)
|
||||||
|
// version7 and larger version has version info
|
||||||
|
if q.v.Ver >= 7 {
|
||||||
|
q.fillVersionInfo(mats[i], dimension)
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate score and decide the lowest score and Draw
|
||||||
|
score := evaluation(mats[i])
|
||||||
|
debugLogf("cur idx: %d, score: %d, current lowest: mats[%d]:%d", i, score, markMatsIdx, lowScore)
|
||||||
|
scoreChan <- maskScore{
|
||||||
|
Score: score,
|
||||||
|
Idx: i,
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = debugDraw(fmt.Sprintf("draft/qrcode_mask_%d.jpeg", i), *mats[i])
|
||||||
|
|
||||||
|
wg.Done()
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(scoreChan)
|
||||||
|
|
||||||
|
for c := range scoreChan {
|
||||||
|
if c.Score < lowScore {
|
||||||
|
lowScore = c.Score
|
||||||
|
markMatsIdx = c.Idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
q.mat = mats[markMatsIdx]
|
||||||
|
}
|
||||||
|
|
||||||
|
// all mask patter and check the maskScore choose the lowest mask result
|
||||||
|
func (q *QRCode) xorMask(m *Matrix, mask *mask) {
|
||||||
|
mask.mat.iter(IterDirection_COLUMN, func(x, y int, v qrvalue) {
|
||||||
|
// skip the empty place
|
||||||
|
if v.qrtype() == QRType_INIT {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v2, _ := m.at(x, y)
|
||||||
|
_ = m.set(x, y, v2.xor(v))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// fillVersionInfo ref to:
|
||||||
|
// https://www.thonky.com/qr-code-tutorial/format-version-tables
|
||||||
|
func (q *QRCode) fillVersionInfo(m *Matrix, dimension int) {
|
||||||
|
bin := q.v.verInfo()
|
||||||
|
|
||||||
|
// from high bit to lowest
|
||||||
|
pos := 0
|
||||||
|
for j := 5; j >= 0; j-- {
|
||||||
|
for i := 1; i <= 3; i++ {
|
||||||
|
if bin.At(pos) {
|
||||||
|
_ = m.set(dimension-8-i, j, QRValue_VERSION_V1)
|
||||||
|
_ = m.set(j, dimension-8-i, QRValue_VERSION_V1)
|
||||||
|
} else {
|
||||||
|
_ = m.set(dimension-8-i, j, QRValue_VERSION_V0)
|
||||||
|
_ = m.set(j, dimension-8-i, QRValue_VERSION_V0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill format info ref to:
|
||||||
|
// https://www.thonky.com/qr-code-tutorial/format-version-tables
|
||||||
|
func (q *QRCode) fillFormatInfo(m *Matrix, mode maskPatternModulo, dimension int) {
|
||||||
|
fmtBSet := q.v.formatInfo(int(mode))
|
||||||
|
debugLogf("fmtBitSet: %s", fmtBSet.String())
|
||||||
|
var (
|
||||||
|
x, y = 0, dimension - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
for pos := 0; pos < 15; pos++ {
|
||||||
|
if fmtBSet.At(pos) {
|
||||||
|
// row
|
||||||
|
_ = m.set(x, 8, QRValue_FORMAT_V1)
|
||||||
|
// column
|
||||||
|
_ = m.set(8, y, QRValue_FORMAT_V1)
|
||||||
|
} else {
|
||||||
|
// row
|
||||||
|
_ = m.set(x, 8, QRValue_FORMAT_V0)
|
||||||
|
// column
|
||||||
|
_ = m.set(8, y, QRValue_FORMAT_V0)
|
||||||
|
}
|
||||||
|
|
||||||
|
x = x + 1
|
||||||
|
y = y - 1
|
||||||
|
|
||||||
|
// row skip
|
||||||
|
if x == 6 {
|
||||||
|
x = 7
|
||||||
|
} else if x == 8 {
|
||||||
|
x = dimension - 8
|
||||||
|
}
|
||||||
|
|
||||||
|
// column skip
|
||||||
|
if y == dimension-8 {
|
||||||
|
y = 8
|
||||||
|
} else if y == 6 {
|
||||||
|
y = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package qrcode
|
||||||
|
|
||||||
|
// samestate judge two matrix qrtype is same with binary semantic.
|
||||||
|
// QRValue_DATA_V0/QRType_INIT only equal to QRValue_DATA_V0, other state are equal to each other.
|
||||||
|
func samestate(s1, s2 qrvalue) bool {
|
||||||
|
return s1.qrbool() == s2.qrbool()
|
||||||
|
}
|
||||||
|
|
||||||
|
func abs(x int) int {
|
||||||
|
if x < 0 {
|
||||||
|
return -x
|
||||||
|
}
|
||||||
|
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(x, y int) int {
|
||||||
|
if x < y {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
||||||
|
func binaryToQRValueSlice(s string) []qrvalue {
|
||||||
|
var states = make([]qrvalue, 0, len(s))
|
||||||
|
for _, c := range s {
|
||||||
|
switch c {
|
||||||
|
case '1':
|
||||||
|
states = append(states, QRValue_DATA_V1)
|
||||||
|
case '0':
|
||||||
|
states = append(states, QRValue_DATA_V0)
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return states
|
||||||
|
}
|
|
@ -0,0 +1,409 @@
|
||||||
|
package qrcode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
// "github.com/skip2/go-qrcode/bitset"
|
||||||
|
"github.com/yeqown/reedsolomon/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ecLevel error correction level
|
||||||
|
type ecLevel int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ErrorCorrectionLow :Level L: 7% error recovery.
|
||||||
|
ErrorCorrectionLow ecLevel = iota + 1
|
||||||
|
|
||||||
|
// ErrorCorrectionMedium :Level M: 15% error recovery. Good default choice.
|
||||||
|
ErrorCorrectionMedium
|
||||||
|
|
||||||
|
// ErrorCorrectionQuart :Level Q: 25% error recovery.
|
||||||
|
ErrorCorrectionQuart
|
||||||
|
|
||||||
|
// ErrorCorrectionHighest :Level H: 30% error recovery.
|
||||||
|
ErrorCorrectionHighest
|
||||||
|
|
||||||
|
formatInfoBitsNum = 15 // format info bits num
|
||||||
|
verInfoBitsNum = 18 // version info length bits num
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errInvalidErrorCorrectionLevel = errors.New("invalid error correction level")
|
||||||
|
errAnalyzeVersionFailed = errors.New("could not match version! " +
|
||||||
|
"check your content length is in limitation of encode mode and error correction level")
|
||||||
|
errMissMatchedVersion = errors.New("could not match version")
|
||||||
|
errMissMatchedEncodeType = errors.New("could not match the encode type")
|
||||||
|
// versions []version
|
||||||
|
// Each QR Code contains a 15-bit Format Information qrbool. The 15 bits
|
||||||
|
// consist of 5 data bits concatenated with 10 error correction bits.
|
||||||
|
//
|
||||||
|
// The 5 data bits consist of:
|
||||||
|
// - 2 bits for the error correction level (L=01, M=00, G=11, H=10).
|
||||||
|
// - 3 bits for the data mask pattern identifier.
|
||||||
|
//
|
||||||
|
// formatBitSequence is a mapping from the 5 data bits to the completed 15-bit
|
||||||
|
// Format Information qrbool.
|
||||||
|
//
|
||||||
|
// For example, a QR Code using error correction level L, and data mask
|
||||||
|
// pattern identifier 001:
|
||||||
|
//
|
||||||
|
// 01 | 001 = 01001 = 0x9
|
||||||
|
// formatBitSequence[0x9].qrCode = 0x72f3 = 111001011110011
|
||||||
|
formatBitSequence = []struct {
|
||||||
|
regular uint32
|
||||||
|
micro uint32
|
||||||
|
}{
|
||||||
|
{0x5412, 0x4445}, {0x5125, 0x4172}, {0x5e7c, 0x4e2b}, {0x5b4b, 0x4b1c},
|
||||||
|
{0x45f9, 0x55ae}, {0x40ce, 0x5099}, {0x4f97, 0x5fc0}, {0x4aa0, 0x5af7},
|
||||||
|
{0x77c4, 0x6793}, {0x72f3, 0x62a4}, {0x7daa, 0x6dfd}, {0x789d, 0x68ca},
|
||||||
|
{0x662f, 0x7678}, {0x6318, 0x734f}, {0x6c41, 0x7c16}, {0x6976, 0x7921},
|
||||||
|
{0x1689, 0x06de}, {0x13be, 0x03e9}, {0x1ce7, 0x0cb0}, {0x19d0, 0x0987},
|
||||||
|
{0x0762, 0x1735}, {0x0255, 0x1202}, {0x0d0c, 0x1d5b}, {0x083b, 0x186c},
|
||||||
|
{0x355f, 0x2508}, {0x3068, 0x203f}, {0x3f31, 0x2f66}, {0x3a06, 0x2a51},
|
||||||
|
{0x24b4, 0x34e3}, {0x2183, 0x31d4}, {0x2eda, 0x3e8d}, {0x2bed, 0x3bba},
|
||||||
|
}
|
||||||
|
|
||||||
|
// QR Codes version 7 and higher contain an 18-bit version Information qrbool,
|
||||||
|
// consisting of a 6 data bits and 12 error correction bits.
|
||||||
|
//
|
||||||
|
// versionBitSequence is a mapping from QR Code version to the completed
|
||||||
|
// 18-bit version Information qrbool.
|
||||||
|
//
|
||||||
|
// For example, a QR code of version 7:
|
||||||
|
// versionBitSequence[0x7] = 0x07c94 = 000111110010010100
|
||||||
|
versionBitSequence = []uint32{
|
||||||
|
0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x07c94,
|
||||||
|
0x085bc, 0x09a99, 0x0a4d3, 0x0bbf6, 0x0c762, 0x0d847, 0x0e60d, 0x0f928,
|
||||||
|
0x10b78, 0x1145d, 0x12a17, 0x13532, 0x149a6, 0x15683, 0x168c9, 0x177ec,
|
||||||
|
0x18ec4, 0x191e1, 0x1afab, 0x1b08e, 0x1cc1a, 0x1d33f, 0x1ed75, 0x1f250,
|
||||||
|
0x209d5, 0x216f0, 0x228ba, 0x2379f, 0x24b0b, 0x2542e, 0x26a64, 0x27541, 0x28c69,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// capacity struct includes data type max capacity
|
||||||
|
type capacity struct {
|
||||||
|
Numeric int `json:"n"` // num capacity
|
||||||
|
AlphaNumeric int `json:"a"` // char capacity
|
||||||
|
Byte int `json:"b"` // byte capacity (utf-8 also)
|
||||||
|
JP int `json:"j"` // Japanese capacity
|
||||||
|
}
|
||||||
|
|
||||||
|
// group contains fields to generate ECBlocks
|
||||||
|
// and append _defaultPadding bit
|
||||||
|
type group struct {
|
||||||
|
// NumBlocks num of blocks
|
||||||
|
NumBlocks int `json:"nbs"`
|
||||||
|
|
||||||
|
// NumDataCodewords Number of data codewords.
|
||||||
|
NumDataCodewords int `json:"ndcs"`
|
||||||
|
|
||||||
|
// ECBlockwordsPerBlock ...
|
||||||
|
ECBlockwordsPerBlock int `json:"ecbs_pb"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// version ...
|
||||||
|
type version struct {
|
||||||
|
// version code 1-40
|
||||||
|
Ver int `json:"ver"`
|
||||||
|
|
||||||
|
// ECLevel error correction 0, 1, 2, 3
|
||||||
|
ECLevel ecLevel `json:"eclv"`
|
||||||
|
|
||||||
|
// Cap includes each type's max capacity (specified by `Ver` and `ecLevel`)
|
||||||
|
// ref to: https://www.thonky.com/qr-code-tutorial/character-capacities
|
||||||
|
Cap capacity `json:"cap"`
|
||||||
|
|
||||||
|
// RemainderBits remainder bits need to append finally
|
||||||
|
RemainderBits int `json:"rembits"`
|
||||||
|
|
||||||
|
// groups info to generate
|
||||||
|
// ref to: https://www.thonky.com/qr-code-tutorial/error-correction-table
|
||||||
|
// numGroup = len(Groups)
|
||||||
|
Groups []group `json:"groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dimension ...
|
||||||
|
func (v version) Dimension() int {
|
||||||
|
return v.Ver*4 + 17
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumTotalCodewords total data codewords
|
||||||
|
func (v version) NumTotalCodewords() int {
|
||||||
|
var total int
|
||||||
|
for _, g := range v.Groups {
|
||||||
|
total = total + (g.NumBlocks * g.NumDataCodewords)
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumGroups ... need group num. ref to version config file
|
||||||
|
func (v version) NumGroups() int {
|
||||||
|
return len(v.Groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TotalNumBlocks ... total data blocks num, ref to version config file
|
||||||
|
func (v version) TotalNumBlocks() int {
|
||||||
|
var total int
|
||||||
|
for _, g := range v.Groups {
|
||||||
|
total = total + g.NumBlocks
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerInfo version info bitset
|
||||||
|
func (v version) verInfo() *binary.Binary {
|
||||||
|
if v.Ver < 7 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := binary.New()
|
||||||
|
result.AppendUint32(versionBitSequence[v.Ver], verInfoBitsNum)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatInfo returns the 15-bit Format Information qrbool for a QR
|
||||||
|
// code.
|
||||||
|
func (v version) formatInfo(maskPattern int) *binary.Binary {
|
||||||
|
formatID := 0
|
||||||
|
|
||||||
|
switch v.ECLevel {
|
||||||
|
case ErrorCorrectionLow:
|
||||||
|
formatID = 0x08 // 0b01000
|
||||||
|
case ErrorCorrectionMedium:
|
||||||
|
formatID = 0x00 // 0b00000
|
||||||
|
case ErrorCorrectionQuart:
|
||||||
|
formatID = 0x18 // 0b11000
|
||||||
|
case ErrorCorrectionHighest:
|
||||||
|
formatID = 0x10 // 0b10000
|
||||||
|
default:
|
||||||
|
log.Panicf("Invalid level %d", v.ECLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if maskPattern < 0 || maskPattern > 7 {
|
||||||
|
log.Panicf("Invalid maskPattern %d", maskPattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatID |= maskPattern & 0x7
|
||||||
|
result := binary.New()
|
||||||
|
result.AppendUint32(formatBitSequence[formatID].regular, formatInfoBitsNum)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var emptyVersion = version{Ver: -1}
|
||||||
|
|
||||||
|
// binarySearchVersion speed up searching target version in versions.
|
||||||
|
// low, high to set the left and right bound of the search range (min:0 to max:159).
|
||||||
|
// compare represents the function to compare the target version with the cursor version.
|
||||||
|
// negative means lower direction, positive means higher direction, zero mean hit.
|
||||||
|
func binarySearchVersion(low, high int, compare func(*version) int) (hit version, found bool) {
|
||||||
|
// left low and high in a valid range
|
||||||
|
if low > high || low > _VERSIONS_ITEM_COUNT || high < 0 {
|
||||||
|
return emptyVersion, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if low < 0 {
|
||||||
|
low = 0
|
||||||
|
}
|
||||||
|
if high >= _VERSIONS_ITEM_COUNT {
|
||||||
|
high = len(versions) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for low <= high {
|
||||||
|
mid := (low + high) / 2
|
||||||
|
r := compare(&versions[mid])
|
||||||
|
if r == 0 {
|
||||||
|
hit = versions[mid]
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if r > 0 {
|
||||||
|
// move toward higher direction
|
||||||
|
low = mid + 1
|
||||||
|
} else {
|
||||||
|
// move toward lower direction
|
||||||
|
high = mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hit, found
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultBinaryCompare built-in compare function for binary search.
|
||||||
|
func defaultBinaryCompare(ver int, ec ecLevel) func(cursor *version) int {
|
||||||
|
return func(cursor *version) int {
|
||||||
|
switch r := ver - cursor.Ver; r {
|
||||||
|
case 0:
|
||||||
|
default:
|
||||||
|
// v is bigger return positive; otherwise return negative.
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(ec - cursor.ECLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadVersion get version config by specified version indicator and error correction level.
|
||||||
|
// we can speed up this process, by shrink the range to search.
|
||||||
|
func loadVersion(lv int, ec ecLevel) version {
|
||||||
|
// each version only has 4 items in versions array,
|
||||||
|
// and them are ordered[ASC] already.
|
||||||
|
high := lv*4 - 1
|
||||||
|
low := (lv - 1) * 4
|
||||||
|
|
||||||
|
for i := low; i <= high; i++ {
|
||||||
|
if versions[i].ECLevel == ec {
|
||||||
|
return versions[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic(errMissMatchedVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// analyzeVersion the raw text, and then decide which version should be chosen
|
||||||
|
// according to the text length , error correction level and encode mode to choose the
|
||||||
|
// closest capacity of version.
|
||||||
|
//
|
||||||
|
// check out http://muyuchengfeng.xyz/%E4%BA%8C%E7%BB%B4%E7%A0%81-%E5%AD%97%E7%AC%A6%E5%AE%B9%E9%87%8F%E8%A1%A8/
|
||||||
|
// for more details.
|
||||||
|
func analyzeVersion(raw []byte, ec ecLevel, mode encMode) (*version, error) {
|
||||||
|
step := 0
|
||||||
|
switch ec {
|
||||||
|
case ErrorCorrectionLow:
|
||||||
|
step = 0
|
||||||
|
case ErrorCorrectionMedium:
|
||||||
|
step = 1
|
||||||
|
case ErrorCorrectionQuart:
|
||||||
|
step = 2
|
||||||
|
case ErrorCorrectionHighest:
|
||||||
|
step = 3
|
||||||
|
default:
|
||||||
|
return nil, errInvalidErrorCorrectionLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
want, mark := len(raw), 0
|
||||||
|
for ; step < 160; step += 4 {
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case EncModeNumeric:
|
||||||
|
mark = versions[step].Cap.Numeric
|
||||||
|
case EncModeAlphanumeric:
|
||||||
|
mark = versions[step].Cap.AlphaNumeric
|
||||||
|
case EncModeByte:
|
||||||
|
mark = versions[step].Cap.Byte
|
||||||
|
case EncModeJP:
|
||||||
|
mark = versions[step].Cap.JP
|
||||||
|
default:
|
||||||
|
return nil, errMissMatchedEncodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
if mark >= want {
|
||||||
|
return &versions[step], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debugLogf("mismatched version, version's length: %d, ec: %v", len(versions), ec)
|
||||||
|
|
||||||
|
return nil, errAnalyzeVersionFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// https://www.thonky.com/qr-code-tutorial/alignment-pattern-locations
|
||||||
|
// DONE(@yeqown): add more version
|
||||||
|
alignPatternLocation = map[int][]int{
|
||||||
|
2: {6, 18},
|
||||||
|
3: {6, 22},
|
||||||
|
4: {6, 26},
|
||||||
|
5: {6, 30},
|
||||||
|
6: {6, 34},
|
||||||
|
7: {6, 22, 38},
|
||||||
|
8: {6, 24, 42},
|
||||||
|
9: {6, 26, 46},
|
||||||
|
10: {6, 28, 50},
|
||||||
|
11: {6, 30, 54},
|
||||||
|
12: {6, 32, 58},
|
||||||
|
13: {6, 34, 62},
|
||||||
|
14: {6, 26, 46, 66},
|
||||||
|
15: {6, 26, 48, 70},
|
||||||
|
16: {6, 26, 50, 74},
|
||||||
|
17: {6, 30, 54, 78},
|
||||||
|
18: {6, 30, 56, 82},
|
||||||
|
19: {6, 30, 58, 86},
|
||||||
|
20: {6, 34, 62, 90},
|
||||||
|
21: {6, 28, 50, 72, 94},
|
||||||
|
22: {6, 26, 50, 74, 98},
|
||||||
|
23: {6, 30, 54, 78, 102},
|
||||||
|
24: {6, 28, 54, 80, 106},
|
||||||
|
25: {6, 32, 58, 84, 110},
|
||||||
|
26: {6, 30, 58, 86, 114},
|
||||||
|
27: {6, 34, 62, 90, 118},
|
||||||
|
28: {6, 26, 50, 74, 98, 122},
|
||||||
|
29: {6, 30, 54, 78, 102, 126},
|
||||||
|
30: {6, 26, 52, 78, 104, 130},
|
||||||
|
31: {6, 30, 56, 82, 108, 134},
|
||||||
|
32: {6, 34, 60, 86, 112, 138},
|
||||||
|
33: {6, 30, 58, 86, 114, 142},
|
||||||
|
34: {6, 34, 62, 90, 118, 146},
|
||||||
|
35: {6, 30, 54, 78, 102, 126, 150},
|
||||||
|
36: {6, 24, 50, 76, 102, 128, 154},
|
||||||
|
37: {6, 28, 54, 80, 106, 132, 158},
|
||||||
|
38: {6, 32, 58, 84, 110, 136, 162},
|
||||||
|
39: {6, 26, 54, 82, 110, 138, 166},
|
||||||
|
40: {6, 30, 58, 86, 114, 142, 170},
|
||||||
|
}
|
||||||
|
|
||||||
|
alignPatternCache = map[int][]loc{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// loc point position(x,y)
|
||||||
|
type loc struct {
|
||||||
|
X int // for width
|
||||||
|
Y int // for height
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadAlignmentPatternLoc ...
|
||||||
|
func loadAlignmentPatternLoc(ver int) (locs []loc) {
|
||||||
|
if ver < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var ok bool
|
||||||
|
if locs, ok = alignPatternCache[ver]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dimension := ver*4 + 17
|
||||||
|
positions, ok := alignPatternLocation[ver]
|
||||||
|
if !ok {
|
||||||
|
panic("could not found align at version: " + strconv.Itoa(ver))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pos1 := range positions {
|
||||||
|
for _, pos2 := range positions {
|
||||||
|
if !valid(pos1, pos2, dimension) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
locs = append(locs, loc{X: pos1, Y: pos2})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alignPatternCache[ver] = locs
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// x, y center position x,y so
|
||||||
|
func valid(x, y, dimension int) bool {
|
||||||
|
// valid left-top
|
||||||
|
if (x-2) < 7 && (y-2) < 7 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// valid right-top
|
||||||
|
if (x+2) > dimension-7 && (y-2) < 7 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// valid left-bottom
|
||||||
|
if (x-2) < 7 && (y+2) > dimension-7 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,25 @@
|
||||||
|
package qrcode
|
||||||
|
|
||||||
|
// Writer is the interface of a QR code writer, it defines the rule of how to
|
||||||
|
// `print` the code image from matrix. There's built-in writer to output into
|
||||||
|
// file, terminal.
|
||||||
|
type Writer interface {
|
||||||
|
// Write writes the code image into itself stream, such as io.Writer,
|
||||||
|
// terminal output stream, and etc
|
||||||
|
Write(mat Matrix) error
|
||||||
|
|
||||||
|
// Close the writer stream if it exists after QRCode.Save() is called.
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Writer = (*nonWriter)(nil)
|
||||||
|
|
||||||
|
type nonWriter struct{}
|
||||||
|
|
||||||
|
func (n nonWriter) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n nonWriter) Write(mat Matrix) error {
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 yeqown
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,82 @@
|
||||||
|
## Standard Writer
|
||||||
|
|
||||||
|
[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/yeqown/go-qrcode/writer/standard)
|
||||||
|
|
||||||
|
Standard Writer is a writer that is used to draw QR Code image into `io.Writer`, normally a file.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
options := []ImageOption{
|
||||||
|
WithBgColorRGBHex("#ffffff"),
|
||||||
|
WithFgColorRGBHex("#000000"),
|
||||||
|
// more ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// New will create file automatically.
|
||||||
|
writer, err := standard.New("filename", options...)
|
||||||
|
|
||||||
|
// or use io.WriteCloser
|
||||||
|
var w io.WriterCloser
|
||||||
|
writer2, err := standard.NewWith(w, options...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```go
|
||||||
|
// WithBgTransparent makes the background transparent.
|
||||||
|
func WithBgTransparent() ImageOption {}
|
||||||
|
|
||||||
|
// WithBgColor background color
|
||||||
|
func WithBgColor(c color.Color) ImageOption {}
|
||||||
|
|
||||||
|
// WithBgColorRGBHex background color
|
||||||
|
func WithBgColorRGBHex(hex string) ImageOption {}
|
||||||
|
|
||||||
|
// WithFgColor QR color
|
||||||
|
func WithFgColor(c color.Color) ImageOption {}
|
||||||
|
|
||||||
|
// WithFgColorRGBHex Hex string to set QR Color
|
||||||
|
func WithFgColorRGBHex(hex string) ImageOption {}
|
||||||
|
|
||||||
|
// WithLogoImage .
|
||||||
|
func WithLogoImage(img image.Image) ImageOption {}
|
||||||
|
|
||||||
|
// WithLogoImageFilePNG load image from file, PNG is required
|
||||||
|
func WithLogoImageFilePNG(f string) ImageOption {}
|
||||||
|
|
||||||
|
// WithLogoImageFileJPEG load image from file, JPEG is required
|
||||||
|
func WithLogoImageFileJPEG(f string) ImageOption {}
|
||||||
|
|
||||||
|
// WithQRWidth specify width of each qr block
|
||||||
|
func WithQRWidth(width uint8) ImageOption {}
|
||||||
|
|
||||||
|
// WithCircleShape use circle shape as rectangle(default)
|
||||||
|
func WithCircleShape() ImageOption {}
|
||||||
|
|
||||||
|
// WithCustomShape use custom shape as rectangle(default)
|
||||||
|
func WithCustomShape(shape IShape) ImageOption {}
|
||||||
|
|
||||||
|
// WithBuiltinImageEncoder option includes: JPEG_FORMAT as default, PNG_FORMAT.
|
||||||
|
// This works like WithBuiltinImageEncoder, the different between them is
|
||||||
|
// formatTyp is enumerated in (JPEG_FORMAT, PNG_FORMAT)
|
||||||
|
func WithBuiltinImageEncoder(format formatTyp) ImageOption
|
||||||
|
|
||||||
|
// WithCustomImageEncoder to use custom image encoder to encode image.Image into
|
||||||
|
// io.Writer
|
||||||
|
func WithCustomImageEncoder(encoder ImageEncoder) ImageOption
|
||||||
|
|
||||||
|
// WithBorderWidth specify the both 4 sides' border width. Notice that
|
||||||
|
// WithBorderWidth(a) means all border width use this variable `a`,
|
||||||
|
// WithBorderWidth(a, b) mean top/bottom equal to `a`, left/right equal to `b`.
|
||||||
|
// WithBorderWidth(a, b, c, d) mean top, right, bottom, left.
|
||||||
|
func WithBorderWidth(widths ...int) ImageOption
|
||||||
|
|
||||||
|
// WithHalftone ...
|
||||||
|
func WithHalftone(path string) ImageOption
|
||||||
|
```
|
||||||
|
|
||||||
|
### extension
|
||||||
|
|
||||||
|
- [How to customize QR Code shape](./how-to-use-custom-shape.md)
|
||||||
|
- [How to customize ImageEncoder](./how-to-use-image-encoder.md)
|
85
vendor/github.com/yeqown/go-qrcode/writer/standard/how-to-use-custom-shape.md
generated
vendored
Normal file
85
vendor/github.com/yeqown/go-qrcode/writer/standard/how-to-use-custom-shape.md
generated
vendored
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
## How to use custom shape
|
||||||
|
|
||||||
|
[Source Code](../../example/with-custom-shape/main.go)
|
||||||
|
|
||||||
|
first step, you must define your own shape to QRCode, which consists of two part:
|
||||||
|
* normal cell (of course, there are many types, separator, timing, alignment patter, data, format and version etc)
|
||||||
|
* finder cell (to help recognizer to locate the matrix's position)
|
||||||
|
|
||||||
|
<img src="../../assets/qrcode_structure.png" align="center" width="50%" />
|
||||||
|
|
||||||
|
```go
|
||||||
|
type IShape interface {
|
||||||
|
// Draw to fill the IShape of qrcode.
|
||||||
|
Draw(ctx *DrawContext)
|
||||||
|
|
||||||
|
// DrawFinder to fill the finder pattern of QRCode, what's finder? google it for more information.
|
||||||
|
DrawFinder(ctx *DrawContext)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Notice:
|
||||||
|
>
|
||||||
|
> if you must be careful to design finder's shape, otherwise qrcode could not be recognized.
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
Now, if you're define your shape like this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func newShape(radiusPercent float64) qrcode.IShape {
|
||||||
|
return &smallerCircle{smallerPercent: radiusPercent}
|
||||||
|
}
|
||||||
|
|
||||||
|
// smallerCircle use smaller circle to qrcode.
|
||||||
|
type smallerCircle struct {
|
||||||
|
smallerPercent float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *smallerCircle) DrawFinder(ctx *qrcode.DrawContext) {
|
||||||
|
// use normal radius to draw finder for that qrcode image can be recognized.
|
||||||
|
backup := sc.smallerPercent
|
||||||
|
sc.smallerPercent = 1.0
|
||||||
|
sc.Draw(ctx)
|
||||||
|
sc.smallerPercent = backup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *smallerCircle) Draw(ctx *qrcode.DrawContext) {
|
||||||
|
w, h := ctx.Edge()
|
||||||
|
upperLeft := ctx.UpperLeft()
|
||||||
|
color := ctx.Color()
|
||||||
|
|
||||||
|
// choose a proper radius values
|
||||||
|
radius := w / 2
|
||||||
|
r2 := h / 2
|
||||||
|
if r2 <= radius {
|
||||||
|
radius = r2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 80 percent smaller
|
||||||
|
radius = int(float64(radius) * sc.smallerPercent)
|
||||||
|
|
||||||
|
cx, cy := upperLeft.X+w/2, upperLeft.Y+h/2 // get center point
|
||||||
|
ctx.DrawCircle(float64(cx), float64(cy), float64(radius))
|
||||||
|
ctx.SetColor(color)
|
||||||
|
ctx.Fill()
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, you can use your shape.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
shape := newShape(0.7)
|
||||||
|
qrc, err := qrcode.New("with-custom-shape", qrcode.WithCustomShape(shape))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = qrc.Save("./smaller.png")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
1
vendor/github.com/yeqown/go-qrcode/writer/standard/how-to-use-image-encoder.md
generated
vendored
Normal file
1
vendor/github.com/yeqown/go-qrcode/writer/standard/how-to-use-image-encoder.md
generated
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
## Customize image encoder
|
35
vendor/github.com/yeqown/go-qrcode/writer/standard/image_format.go
generated
vendored
Normal file
35
vendor/github.com/yeqown/go-qrcode/writer/standard/image_format.go
generated
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package standard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type formatTyp uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// JPEG_FORMAT as default output file format.
|
||||||
|
JPEG_FORMAT formatTyp = iota
|
||||||
|
// PNG_FORMAT .
|
||||||
|
PNG_FORMAT
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImageEncoder is an interface which describes the rule how to encode image.Image into io.Writer
|
||||||
|
type ImageEncoder interface {
|
||||||
|
// Encode specify which format to encode image into io.Writer.
|
||||||
|
Encode(w io.Writer, img image.Image) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type jpegEncoder struct{}
|
||||||
|
|
||||||
|
func (j jpegEncoder) Encode(w io.Writer, img image.Image) error {
|
||||||
|
return jpeg.Encode(w, img, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
type pngEncoder struct{}
|
||||||
|
|
||||||
|
func (j pngEncoder) Encode(w io.Writer, img image.Image) error {
|
||||||
|
return png.Encode(w, img)
|
||||||
|
}
|
190
vendor/github.com/yeqown/go-qrcode/writer/standard/image_option.go
generated
vendored
Normal file
190
vendor/github.com/yeqown/go-qrcode/writer/standard/image_option.go
generated
vendored
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
package standard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/yeqown/go-qrcode/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ImageOption interface {
|
||||||
|
apply(o *outputImageOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultOutputImageOption default output image background color and etc options
|
||||||
|
func defaultOutputImageOption() *outputImageOptions {
|
||||||
|
return &outputImageOptions{
|
||||||
|
bgColor: color_WHITE, // white
|
||||||
|
bgTransparent: false, // not transparent
|
||||||
|
qrColor: color_BLACK, // black
|
||||||
|
logo: nil, //
|
||||||
|
qrWidth: 20, //
|
||||||
|
shape: _shapeRectangle, //
|
||||||
|
imageEncoder: jpegEncoder{},
|
||||||
|
borderWidths: [4]int{_defaultPadding, _defaultPadding, _defaultPadding, _defaultPadding},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// outputImageOptions to output QR code image
|
||||||
|
type outputImageOptions struct {
|
||||||
|
// bgColor is the background color of the QR code image.
|
||||||
|
bgColor color.RGBA
|
||||||
|
// bgTransparent only affects on PNG_FORMAT
|
||||||
|
bgTransparent bool
|
||||||
|
|
||||||
|
// qrColor is the foreground color of the QR code.
|
||||||
|
qrColor color.RGBA
|
||||||
|
|
||||||
|
// logo this icon image would be put the center of QR Code image
|
||||||
|
// NOTE: logo only should have 1/5 size of QRCode image
|
||||||
|
logo image.Image
|
||||||
|
|
||||||
|
// qrWidth width of each qr block
|
||||||
|
qrWidth int
|
||||||
|
|
||||||
|
// shape means how to draw the shape of each cell.
|
||||||
|
shape IShape
|
||||||
|
|
||||||
|
// imageEncoder specify which file format would be encoded the QR image.
|
||||||
|
imageEncoder ImageEncoder
|
||||||
|
|
||||||
|
// borderWidths indicates the border width of the output image. the order is
|
||||||
|
// top, right, bottom, left same as the WithBorder
|
||||||
|
borderWidths [4]int
|
||||||
|
|
||||||
|
// halftoneImg is the halftone image for the output image.
|
||||||
|
halftoneImg image.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
func (oo *outputImageOptions) backgroundColor() color.RGBA {
|
||||||
|
if oo == nil {
|
||||||
|
return color_WHITE
|
||||||
|
}
|
||||||
|
|
||||||
|
if oo.bgTransparent {
|
||||||
|
(&oo.bgColor).A = 0x00
|
||||||
|
}
|
||||||
|
|
||||||
|
return oo.bgColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (oo *outputImageOptions) logoImage() image.Image {
|
||||||
|
if oo == nil || oo.logo == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return oo.logo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (oo *outputImageOptions) qrBlockWidth() int {
|
||||||
|
if oo == nil || (oo.qrWidth <= 0 || oo.qrWidth > 255) {
|
||||||
|
return 20
|
||||||
|
}
|
||||||
|
|
||||||
|
return oo.qrWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (oo *outputImageOptions) getShape() IShape {
|
||||||
|
if oo == nil || oo.shape == nil {
|
||||||
|
return _shapeRectangle
|
||||||
|
}
|
||||||
|
|
||||||
|
return oo.shape
|
||||||
|
}
|
||||||
|
|
||||||
|
// preCalculateAttribute this function must reference to draw function.
|
||||||
|
func (oo *outputImageOptions) preCalculateAttribute(dimension int) *Attribute {
|
||||||
|
if oo == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
top, right, bottom, left := oo.borderWidths[0], oo.borderWidths[1], oo.borderWidths[2], oo.borderWidths[3]
|
||||||
|
return &Attribute{
|
||||||
|
W: dimension*oo.qrBlockWidth() + right + left,
|
||||||
|
H: dimension*oo.qrBlockWidth() + top + bottom,
|
||||||
|
Borders: oo.borderWidths,
|
||||||
|
BlockWidth: oo.qrBlockWidth(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
color_WHITE = parseFromHex("#ffffff")
|
||||||
|
color_BLACK = parseFromHex("#000000")
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// _STATE_MAPPING mapping matrix.State to color.RGBA in debug mode.
|
||||||
|
_STATE_MAPPING = map[qrcode.QRType]color.RGBA{
|
||||||
|
qrcode.QRType_INIT: parseFromHex("#ffffff"), // [bg]
|
||||||
|
qrcode.QRType_DATA: parseFromHex("#cdc9c3"), // [bg]
|
||||||
|
qrcode.QRType_VERSION: parseFromHex("#000000"), // [fg]
|
||||||
|
qrcode.QRType_FORMAT: parseFromHex("#444444"), // [fg]
|
||||||
|
qrcode.QRType_FINDER: parseFromHex("#555555"), // [fg]
|
||||||
|
qrcode.QRType_DARK: parseFromHex("#2BA859"), // [fg]
|
||||||
|
qrcode.QRType_SPLITTER: parseFromHex("#2BA859"), // [fg]
|
||||||
|
qrcode.QRType_TIMING: parseFromHex("#000000"), // [fg]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// translateToRGBA get color.RGBA by value State, if not found, return outputImageOptions.qrColor.
|
||||||
|
// NOTE: this function decides the state should use qrColor or bgColor.
|
||||||
|
func (oo *outputImageOptions) translateToRGBA(v qrcode.QRValue) (rgba color.RGBA) {
|
||||||
|
// TODO(@yeqown): use _STATE_MAPPING to replace this function while in debug mode
|
||||||
|
// or some special flag.
|
||||||
|
if v.IsSet() {
|
||||||
|
rgba = oo.qrColor
|
||||||
|
return rgba
|
||||||
|
}
|
||||||
|
|
||||||
|
if oo.bgTransparent {
|
||||||
|
(&oo.bgColor).A = 0x00
|
||||||
|
}
|
||||||
|
rgba = oo.bgColor
|
||||||
|
|
||||||
|
return rgba
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFromHex convert hex string into color.RGBA
|
||||||
|
func parseFromHex(s string) color.RGBA {
|
||||||
|
c := color.RGBA{
|
||||||
|
R: 0,
|
||||||
|
G: 0,
|
||||||
|
B: 0,
|
||||||
|
A: 0xff,
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
switch len(s) {
|
||||||
|
case 7:
|
||||||
|
_, err = fmt.Sscanf(s, "#%02x%02x%02x", &c.R, &c.G, &c.B)
|
||||||
|
case 4:
|
||||||
|
_, err = fmt.Sscanf(s, "#%1x%1x%1x", &c.R, &c.G, &c.B)
|
||||||
|
// Double the hex digits:
|
||||||
|
c.R *= 17
|
||||||
|
c.G *= 17
|
||||||
|
c.B *= 17
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("invalid length, must be 7 or 4")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFromColor(c color.Color) color.RGBA {
|
||||||
|
rgba, ok := c.(color.RGBA)
|
||||||
|
if ok {
|
||||||
|
return rgba
|
||||||
|
}
|
||||||
|
|
||||||
|
r, g, b, a := c.RGBA()
|
||||||
|
return color.RGBA{
|
||||||
|
R: uint8(r),
|
||||||
|
G: uint8(g),
|
||||||
|
B: uint8(b),
|
||||||
|
A: uint8(a),
|
||||||
|
}
|
||||||
|
}
|
219
vendor/github.com/yeqown/go-qrcode/writer/standard/image_option_api.go
generated
vendored
Normal file
219
vendor/github.com/yeqown/go-qrcode/writer/standard/image_option_api.go
generated
vendored
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
package standard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/yeqown/go-qrcode/writer/standard/imgkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// funcOption wraps a function that modifies outputImageOptions into an
|
||||||
|
// implementation of the ImageOption interface.
|
||||||
|
type funcOption struct {
|
||||||
|
f func(oo *outputImageOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fo *funcOption) apply(oo *outputImageOptions) {
|
||||||
|
fo.f(oo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFuncOption(f func(oo *outputImageOptions)) *funcOption {
|
||||||
|
return &funcOption{
|
||||||
|
f: f,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBgTransparent makes the background transparent.
|
||||||
|
func WithBgTransparent() ImageOption {
|
||||||
|
return newFuncOption(func(oo *outputImageOptions) {
|
||||||
|
oo.bgTransparent = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBgColor background color
|
||||||
|
func WithBgColor(c color.Color) ImageOption {
|
||||||
|
return newFuncOption(func(oo *outputImageOptions) {
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oo.bgColor = parseFromColor(c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBgColorRGBHex background color
|
||||||
|
func WithBgColorRGBHex(hex string) ImageOption {
|
||||||
|
return newFuncOption(func(oo *outputImageOptions) {
|
||||||
|
if hex == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oo.bgColor = parseFromHex(hex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFgColor QR color
|
||||||
|
func WithFgColor(c color.Color) ImageOption {
|
||||||
|
return newFuncOption(func(oo *outputImageOptions) {
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oo.qrColor = parseFromColor(c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFgColorRGBHex Hex string to set QR Color
|
||||||
|
func WithFgColorRGBHex(hex string) ImageOption {
|
||||||
|
return newFuncOption(func(oo *outputImageOptions) {
|
||||||
|
oo.qrColor = parseFromHex(hex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLogoImage image should only has 1/5 width of QRCode at most
|
||||||
|
func WithLogoImage(img image.Image) ImageOption {
|
||||||
|
return newFuncOption(func(oo *outputImageOptions) {
|
||||||
|
if img == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oo.logo = img
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLogoImageFileJPEG load image from file, jpeg is required.
|
||||||
|
// image should only have 1/5 width of QRCode at most
|
||||||
|
func WithLogoImageFileJPEG(f string) ImageOption {
|
||||||
|
return newFuncOption(func(oo *outputImageOptions) {
|
||||||
|
fd, err := os.Open(f)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("could not open file(%s), error=%v\n", f, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := jpeg.Decode(fd)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("could not open file(%s), error=%v\n", f, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oo.logo = img
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLogoImageFilePNG load image from file, PNG is required.
|
||||||
|
// image should only have 1/5 width of QRCode at most
|
||||||
|
func WithLogoImageFilePNG(f string) ImageOption {
|
||||||
|
return newFuncOption(func(oo *outputImageOptions) {
|
||||||
|
fd, err := os.Open(f)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Open file(%s) failed: %v\n", f, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := png.Decode(fd)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Decode file(%s) as PNG failed: %v\n", f, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oo.logo = img
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithQRWidth specify width of each qr block
|
||||||
|
func WithQRWidth(width uint8) ImageOption {
|
||||||
|
return newFuncOption(func(oo *outputImageOptions) {
|
||||||
|
oo.qrWidth = int(width)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCircleShape use circle shape as rectangle(default)
|
||||||
|
func WithCircleShape() ImageOption {
|
||||||
|
return newFuncOption(func(oo *outputImageOptions) {
|
||||||
|
oo.shape = _shapeCircle
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCustomShape use custom shape as rectangle(default)
|
||||||
|
func WithCustomShape(shape IShape) ImageOption {
|
||||||
|
return newFuncOption(func(oo *outputImageOptions) {
|
||||||
|
oo.shape = shape
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBuiltinImageEncoder option includes: JPEG_FORMAT as default, PNG_FORMAT.
|
||||||
|
// This works like WithBuiltinImageEncoder, the different between them is
|
||||||
|
// formatTyp is enumerated in (JPEG_FORMAT, PNG_FORMAT)
|
||||||
|
func WithBuiltinImageEncoder(format formatTyp) ImageOption {
|
||||||
|
return newFuncOption(func(oo *outputImageOptions) {
|
||||||
|
var encoder ImageEncoder
|
||||||
|
switch format {
|
||||||
|
case JPEG_FORMAT:
|
||||||
|
encoder = jpegEncoder{}
|
||||||
|
case PNG_FORMAT:
|
||||||
|
encoder = pngEncoder{}
|
||||||
|
default:
|
||||||
|
panic("Not supported file format")
|
||||||
|
}
|
||||||
|
|
||||||
|
oo.imageEncoder = encoder
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCustomImageEncoder to use custom image encoder to encode image.Image into
|
||||||
|
// io.Writer
|
||||||
|
func WithCustomImageEncoder(encoder ImageEncoder) ImageOption {
|
||||||
|
return newFuncOption(func(oo *outputImageOptions) {
|
||||||
|
if encoder == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oo.imageEncoder = encoder
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBorderWidth specify the both 4 sides' border width. Notice that
|
||||||
|
// WithBorderWidth(a) means all border width use this variable `a`,
|
||||||
|
// WithBorderWidth(a, b) mean top/bottom equal to `a`, left/right equal to `b`.
|
||||||
|
// WithBorderWidth(a, b, c, d) mean top, right, bottom, left.
|
||||||
|
func WithBorderWidth(widths ...int) ImageOption {
|
||||||
|
apply := func(arr *[4]int, top, right, bottom, left int) {
|
||||||
|
arr[0] = top
|
||||||
|
arr[1] = right
|
||||||
|
arr[2] = bottom
|
||||||
|
arr[3] = left
|
||||||
|
}
|
||||||
|
|
||||||
|
return newFuncOption(func(oo *outputImageOptions) {
|
||||||
|
n := len(widths)
|
||||||
|
switch n {
|
||||||
|
case 0:
|
||||||
|
apply(&oo.borderWidths, _defaultPadding, _defaultPadding, _defaultPadding, _defaultPadding)
|
||||||
|
case 1:
|
||||||
|
apply(&oo.borderWidths, widths[0], widths[0], widths[0], widths[0])
|
||||||
|
case 2, 3:
|
||||||
|
apply(&oo.borderWidths, widths[0], widths[1], widths[0], widths[1])
|
||||||
|
default:
|
||||||
|
// 4+
|
||||||
|
apply(&oo.borderWidths, widths[0], widths[1], widths[2], widths[3])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHalftone ...
|
||||||
|
func WithHalftone(path string) ImageOption {
|
||||||
|
return newFuncOption(func(oo *outputImageOptions) {
|
||||||
|
srcImg, err := imgkit.Read(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Read halftone image failed: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oo.halftoneImg = srcImg
|
||||||
|
})
|
||||||
|
}
|
84
vendor/github.com/yeqown/go-qrcode/writer/standard/image_qr_shape.go
generated
vendored
Normal file
84
vendor/github.com/yeqown/go-qrcode/writer/standard/image_qr_shape.go
generated
vendored
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package standard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/fogleman/gg"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_shapeRectangle IShape = rectangle{}
|
||||||
|
_shapeCircle IShape = circle{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type IShape interface {
|
||||||
|
// Draw the shape of QRCode block in IShape implemented way.
|
||||||
|
Draw(ctx *DrawContext)
|
||||||
|
|
||||||
|
// DrawFinder to fill the finder pattern of QRCode, what's finder? google it for more information.
|
||||||
|
DrawFinder(ctx *DrawContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrawContext is a rectangle area
|
||||||
|
type DrawContext struct {
|
||||||
|
*gg.Context
|
||||||
|
|
||||||
|
x, y float64
|
||||||
|
w, h int
|
||||||
|
|
||||||
|
color color.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpperLeft returns the point which indicates the upper left position.
|
||||||
|
func (dc *DrawContext) UpperLeft() (dx, dy float64) {
|
||||||
|
return dc.x, dc.y
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edge returns width and height of each shape could take at most.
|
||||||
|
func (dc *DrawContext) Edge() (width, height int) {
|
||||||
|
return dc.w, dc.h
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color returns the color which should be fill into the shape. Note that if you're not
|
||||||
|
// using this color but your coded color.Color, some ImageOption functions those set foreground color
|
||||||
|
// would take no effect.
|
||||||
|
func (dc *DrawContext) Color() color.Color {
|
||||||
|
return dc.color
|
||||||
|
}
|
||||||
|
|
||||||
|
// rectangle IShape
|
||||||
|
type rectangle struct{}
|
||||||
|
|
||||||
|
func (r rectangle) Draw(c *DrawContext) {
|
||||||
|
// FIXED(@yeqown): miss parameter of DrawRectangle
|
||||||
|
c.DrawRectangle(c.x, c.y, float64(c.w), float64(c.h))
|
||||||
|
c.SetColor(c.color)
|
||||||
|
c.Fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r rectangle) DrawFinder(ctx *DrawContext) {
|
||||||
|
r.Draw(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// circle IShape
|
||||||
|
type circle struct{}
|
||||||
|
|
||||||
|
// Draw
|
||||||
|
// FIXED: Draw could not draw circle
|
||||||
|
func (r circle) Draw(c *DrawContext) {
|
||||||
|
// choose a proper radius values
|
||||||
|
radius := c.w / 2
|
||||||
|
r2 := c.h / 2
|
||||||
|
if r2 <= radius {
|
||||||
|
radius = r2
|
||||||
|
}
|
||||||
|
|
||||||
|
cx, cy := c.x+float64(c.w)/2.0, c.y+float64(c.h)/2.0 // get center point
|
||||||
|
c.DrawCircle(cx, cy, float64(radius))
|
||||||
|
c.SetColor(c.color)
|
||||||
|
c.Fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r circle) DrawFinder(ctx *DrawContext) {
|
||||||
|
r.Draw(ctx)
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package imgkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Read reads an image from a file. only support PNG and JPEG yet.
|
||||||
|
func Read(path string) (img image.Image, err error) {
|
||||||
|
fd, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to open file")
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
|
img, _, err = image.Decode(fd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to decode image")
|
||||||
|
}
|
||||||
|
|
||||||
|
return img, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves the image to the given path.
|
||||||
|
func Save(img image.Image, filename string) error {
|
||||||
|
fd, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
|
switch filepath.Ext(filename) {
|
||||||
|
case ".jpg", ".jpeg":
|
||||||
|
err = jpeg.Encode(fd, img, nil)
|
||||||
|
case ".png":
|
||||||
|
err = png.Encode(fd, img)
|
||||||
|
default:
|
||||||
|
err = errors.New("unsupported image format, jpg or png only")
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
57
vendor/github.com/yeqown/go-qrcode/writer/standard/imgkit/process.go
generated
vendored
Normal file
57
vendor/github.com/yeqown/go-qrcode/writer/standard/imgkit/process.go
generated
vendored
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package imgkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Binaryzation process image with threshold value (0-255) and return new image.
|
||||||
|
func Binaryzation(src image.Image, threshold uint8) image.Image {
|
||||||
|
if threshold < 0 || threshold > 255 {
|
||||||
|
threshold = 128
|
||||||
|
}
|
||||||
|
|
||||||
|
gray := Gray(src)
|
||||||
|
bounds := src.Bounds()
|
||||||
|
height, width := bounds.Max.Y-bounds.Min.Y, bounds.Max.X-bounds.Min.X
|
||||||
|
|
||||||
|
for i := 0; i < height; i++ {
|
||||||
|
for j := 0; j < width; j++ {
|
||||||
|
// var rgb int = int(gray[i][j][0]) + int(gray[i][j][1]) + int(gray[i][j][2])
|
||||||
|
if gray.At(j, i).(color.Gray).Y > threshold {
|
||||||
|
gray.Set(j, i, color.White)
|
||||||
|
} else {
|
||||||
|
gray.Set(j, i, color.Black)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gray
|
||||||
|
}
|
||||||
|
|
||||||
|
func Gray(src image.Image) *image.Gray {
|
||||||
|
bounds := src.Bounds()
|
||||||
|
height, width := bounds.Max.Y-bounds.Min.Y, bounds.Max.X-bounds.Min.X
|
||||||
|
gray := image.NewGray(bounds)
|
||||||
|
|
||||||
|
for i := 0; i < height; i++ {
|
||||||
|
for j := 0; j < width; j++ {
|
||||||
|
c := color.GrayModel.Convert(src.At(j, i))
|
||||||
|
gray.SetGray(j, i, c.(color.Gray))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gray
|
||||||
|
}
|
||||||
|
|
||||||
|
func Scale(src image.Image, rect image.Rectangle, scale draw.Scaler) image.Image {
|
||||||
|
if scale == nil {
|
||||||
|
scale = draw.ApproxBiLinear
|
||||||
|
}
|
||||||
|
|
||||||
|
dst := image.NewRGBA(rect)
|
||||||
|
scale.Scale(dst, rect, src, src.Bounds(), draw.Over, nil)
|
||||||
|
return dst
|
||||||
|
}
|
|
@ -0,0 +1,219 @@
|
||||||
|
package standard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/yeqown/go-qrcode/v2"
|
||||||
|
"github.com/yeqown/go-qrcode/writer/standard/imgkit"
|
||||||
|
|
||||||
|
"github.com/fogleman/gg"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ qrcode.Writer = (*Writer)(nil)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNilWriter = errors.New("nil writer")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Writer is a writer that writes QR Code to io.Writer.
|
||||||
|
type Writer struct {
|
||||||
|
option *outputImageOptions
|
||||||
|
|
||||||
|
closer io.WriteCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a standard writer.
|
||||||
|
func New(filename string, opts ...ImageOption) (*Writer, error) {
|
||||||
|
if _, err := os.Stat(filename); err != nil && os.IsExist(err) {
|
||||||
|
// custom path got: "file exists"
|
||||||
|
log.Printf("could not find path: %s, then save to %s", filename, _defaultFilename)
|
||||||
|
filename = _defaultFilename
|
||||||
|
}
|
||||||
|
|
||||||
|
fd, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "create file failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewWithWriter(fd, opts...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWithWriter(writeCloser io.WriteCloser, opts ...ImageOption) *Writer {
|
||||||
|
dst := defaultOutputImageOption()
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt.apply(dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
if writeCloser == nil {
|
||||||
|
panic("writeCloser could not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Writer{
|
||||||
|
option: dst,
|
||||||
|
closer: writeCloser,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
_defaultFilename = "default.jpeg"
|
||||||
|
_defaultPadding = 40
|
||||||
|
)
|
||||||
|
|
||||||
|
func (w Writer) Write(mat qrcode.Matrix) error {
|
||||||
|
return drawTo(w.closer, mat, w.option)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w Writer) Close() error {
|
||||||
|
if w.closer == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.closer.Close(); !errors.Is(err, os.ErrClosed) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w Writer) Attribute(dimension int) *Attribute {
|
||||||
|
return w.option.preCalculateAttribute(dimension)
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawTo(w io.Writer, mat qrcode.Matrix, option *outputImageOptions) (err error) {
|
||||||
|
if option == nil {
|
||||||
|
option = defaultOutputImageOption()
|
||||||
|
}
|
||||||
|
|
||||||
|
if w == nil {
|
||||||
|
return ErrNilWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
img := draw(mat, option)
|
||||||
|
|
||||||
|
// DONE(@yeqown): support file format specified config option
|
||||||
|
if err = option.imageEncoder.Encode(w, img); err != nil {
|
||||||
|
err = fmt.Errorf("imageEncoder.Encode failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw deal QRCode's matrix to be an image.Image. Notice that if anyone changed this function,
|
||||||
|
// please also check the function outputImageOptions.preCalculateAttribute().
|
||||||
|
func draw(mat qrcode.Matrix, opt *outputImageOptions) image.Image {
|
||||||
|
top, right, bottom, left := opt.borderWidths[0], opt.borderWidths[1], opt.borderWidths[2], opt.borderWidths[3]
|
||||||
|
// closer as image width, h as image height
|
||||||
|
w := mat.Width()*opt.qrBlockWidth() + left + right
|
||||||
|
h := mat.Height()*opt.qrBlockWidth() + top + bottom
|
||||||
|
dc := gg.NewContext(w, h)
|
||||||
|
|
||||||
|
// draw background
|
||||||
|
dc.SetColor(opt.backgroundColor())
|
||||||
|
dc.DrawRectangle(0, 0, float64(w), float64(h))
|
||||||
|
dc.Fill()
|
||||||
|
|
||||||
|
// qrcode block draw context
|
||||||
|
ctx := &DrawContext{
|
||||||
|
Context: dc,
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
w: opt.qrBlockWidth(),
|
||||||
|
h: opt.qrBlockWidth(),
|
||||||
|
color: color.Black,
|
||||||
|
}
|
||||||
|
shape := opt.getShape()
|
||||||
|
|
||||||
|
var (
|
||||||
|
halftoneImg image.Image
|
||||||
|
halftoneW = float64(opt.qrBlockWidth()) / 3.0
|
||||||
|
)
|
||||||
|
if opt.halftoneImg != nil {
|
||||||
|
halftoneImg = imgkit.Binaryzation(
|
||||||
|
imgkit.Scale(opt.halftoneImg, image.Rect(0, 0, mat.Width()*3, mat.Width()*3), nil),
|
||||||
|
60,
|
||||||
|
)
|
||||||
|
|
||||||
|
//_ = imgkit.Save(halftoneImg, "mask.jpeg")
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate the matrix to Draw each pixel
|
||||||
|
mat.Iterate(qrcode.IterDirection_ROW, func(x int, y int, v qrcode.QRValue) {
|
||||||
|
// Draw the block
|
||||||
|
ctx.x, ctx.y = float64(x*opt.qrBlockWidth()+left), float64(y*opt.qrBlockWidth()+top)
|
||||||
|
ctx.w, ctx.h = opt.qrBlockWidth(), opt.qrBlockWidth()
|
||||||
|
ctx.color = opt.translateToRGBA(v)
|
||||||
|
|
||||||
|
// DONE(@yeqown): make this abstract to Shapes
|
||||||
|
switch typ := v.Type(); typ {
|
||||||
|
case qrcode.QRType_FINDER:
|
||||||
|
shape.DrawFinder(ctx)
|
||||||
|
case qrcode.QRType_DATA:
|
||||||
|
if halftoneImg == nil {
|
||||||
|
shape.Draw(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx2 := &DrawContext{
|
||||||
|
Context: ctx.Context,
|
||||||
|
w: int(halftoneW),
|
||||||
|
h: int(halftoneW),
|
||||||
|
}
|
||||||
|
// only halftone image enabled and current block is Data.
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
for j := 0; j < 3; j++ {
|
||||||
|
ctx2.x, ctx2.y = ctx.x+float64(i)*halftoneW, ctx.y+float64(j)*halftoneW
|
||||||
|
ctx2.color = halftoneImg.At(x*3+i, y*3+j)
|
||||||
|
if i == 1 && j == 1 {
|
||||||
|
ctx2.color = ctx.color
|
||||||
|
// only center block keep the origin color.
|
||||||
|
}
|
||||||
|
shape.Draw(ctx2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
shape.Draw(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EOFn
|
||||||
|
})
|
||||||
|
|
||||||
|
// DONE(@yeqown): add logo image
|
||||||
|
if opt.logoImage() != nil {
|
||||||
|
// Draw logo image into rgba
|
||||||
|
bound := opt.logo.Bounds()
|
||||||
|
upperLeft, lowerRight := bound.Min, bound.Max
|
||||||
|
logoWidth, logoHeight := lowerRight.X-upperLeft.X, lowerRight.Y-upperLeft.Y
|
||||||
|
|
||||||
|
if !validLogoImage(w, h, logoWidth, logoHeight) {
|
||||||
|
log.Printf("w=%d, h=%d, logoW=%d, logoH=%d, logo is over than 1/5 of QRCode \n",
|
||||||
|
w, h, logoWidth, logoHeight)
|
||||||
|
goto done
|
||||||
|
}
|
||||||
|
|
||||||
|
// DONE(@yeqown): calculate the xOffset and yOffset which point(xOffset, yOffset)
|
||||||
|
//should icon upper-left to start
|
||||||
|
dc.DrawImage(opt.logoImage(), (w-logoWidth)/2, (h-logoHeight)/2)
|
||||||
|
}
|
||||||
|
done:
|
||||||
|
return dc.Image()
|
||||||
|
}
|
||||||
|
|
||||||
|
func validLogoImage(qrWidth, qrHeight, logoWidth, logoHeight int) bool {
|
||||||
|
return qrWidth >= 5*logoWidth && qrHeight >= 5*logoHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attribute contains basic information of generated image.
|
||||||
|
type Attribute struct {
|
||||||
|
// width and height of image
|
||||||
|
W, H int
|
||||||
|
// in the order of "top, right, bottom, left"
|
||||||
|
Borders [4]int
|
||||||
|
// the length of block edges
|
||||||
|
BlockWidth int
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
# reed solomon encoding lib
|
|
@ -0,0 +1,272 @@
|
||||||
|
// Package binary ...
|
||||||
|
// thanks to https://github.com/skip2/go-qrcode/blob/master/bitset/bitset.go
|
||||||
|
// I cannot do any better for now, so I just learn and write it again~
|
||||||
|
package binary
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
byteTrue byte = '1'
|
||||||
|
byteFalse byte = '0'
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// format string
|
||||||
|
format = "Binary length: %d, bits: %s"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New ...
|
||||||
|
func New(booleans ...bool) *Binary {
|
||||||
|
b := &Binary{
|
||||||
|
bits: make([]byte, 0),
|
||||||
|
lenBits: 0,
|
||||||
|
}
|
||||||
|
b.AppendBools(booleans...)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFromBinaryString ... generate Bitset from binary string
|
||||||
|
// auto get length
|
||||||
|
func NewFromBinaryString(s string) (*Binary, error) {
|
||||||
|
var n = len(s) / 8
|
||||||
|
if len(s)%8 != 0 {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
|
||||||
|
b := &Binary{
|
||||||
|
bits: make([]byte, n), // prealloc memory, reducing useless space
|
||||||
|
lenBits: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range s {
|
||||||
|
switch c {
|
||||||
|
case '1':
|
||||||
|
b.AppendBools(true)
|
||||||
|
case '0':
|
||||||
|
b.AppendBools(false)
|
||||||
|
case ' ':
|
||||||
|
// skip space blank
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
err := fmt.Errorf("invalid char %c in NewFromBinaryString", c)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary struct contains bits stream and methods to be called from outside
|
||||||
|
// exsample:
|
||||||
|
// b.Len()
|
||||||
|
// b.Subset(start, end)
|
||||||
|
// b.At(pos)
|
||||||
|
type Binary struct {
|
||||||
|
bits []byte // 1byte = 8bit
|
||||||
|
lenBits int // len(bits) * 8
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureCapacity ensures the Bitset can store an additional |numBits|.
|
||||||
|
//
|
||||||
|
// The underlying array is expanded if necessary. To prevent frequent
|
||||||
|
// reallocation, expanding the underlying array at least doubles its capacity.
|
||||||
|
//
|
||||||
|
// then no need to use append ~ will no panic (out of range)
|
||||||
|
func (b *Binary) ensureCapacity(numBits int) {
|
||||||
|
numBits += b.lenBits
|
||||||
|
|
||||||
|
newNumBytes := numBits / 8
|
||||||
|
if numBits%8 != 0 {
|
||||||
|
newNumBytes++
|
||||||
|
}
|
||||||
|
|
||||||
|
// if larger enough
|
||||||
|
if len(b.bits) >= newNumBytes {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// larger capcity, about 3 times of current capcity
|
||||||
|
b.bits = append(b.bits, make([]byte, newNumBytes+2*len(b.bits))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// At .get boolean value from
|
||||||
|
func (b *Binary) At(pos int) bool {
|
||||||
|
if pos < 0 || pos >= b.lenBits {
|
||||||
|
panic("out range of bits")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (b.bits[pos/8]&(0x80>>uint(pos%8)) != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subset do the same work like slice[start:end]
|
||||||
|
func (b *Binary) Subset(start, end int) (*Binary, error) {
|
||||||
|
if start > end || end > b.lenBits {
|
||||||
|
err := fmt.Errorf("Out of range start=%d end=%d lenBits=%d", start, end, b.lenBits)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := New()
|
||||||
|
result.ensureCapacity(end - start)
|
||||||
|
|
||||||
|
for i := start; i < end; i++ {
|
||||||
|
if b.At(i) {
|
||||||
|
result.bits[result.lenBits/8] |= 0x80 >> uint(result.lenBits%8)
|
||||||
|
}
|
||||||
|
result.lenBits++
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append other bitset link another Bitset to after the b
|
||||||
|
func (b *Binary) Append(other *Binary) {
|
||||||
|
b.ensureCapacity(other.Len())
|
||||||
|
|
||||||
|
for i := 0; i < other.lenBits; i++ {
|
||||||
|
if other.At(i) {
|
||||||
|
b.bits[b.lenBits/8] |= 0x80 >> uint(b.lenBits%8)
|
||||||
|
}
|
||||||
|
b.lenBits++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendUint32 other bitset link another Bitset to after the b
|
||||||
|
func (b *Binary) AppendUint32(value uint32, numBits int) {
|
||||||
|
b.ensureCapacity(numBits)
|
||||||
|
|
||||||
|
if numBits > 32 {
|
||||||
|
log.Panicf("numBits %d out of range 0-32", numBits)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := numBits - 1; i >= 0; i-- {
|
||||||
|
if value&(1<<uint(i)) != 0 {
|
||||||
|
b.bits[b.lenBits/8] |= 0x80 >> uint(b.lenBits%8)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.lenBits++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendBytes ...
|
||||||
|
func (b *Binary) AppendBytes(byts ...byte) {
|
||||||
|
for _, byt := range byts {
|
||||||
|
b.AppendByte(byt, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendByte ... specified num bits to append
|
||||||
|
func (b *Binary) AppendByte(byt byte, numBits int) error {
|
||||||
|
if numBits > 8 || numBits < 0 {
|
||||||
|
return fmt.Errorf("numBits out of range 0-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ensureCapacity(numBits)
|
||||||
|
|
||||||
|
// append bit in byte
|
||||||
|
for i := numBits - 1; i >= 0; i-- {
|
||||||
|
// 0x01 << left shift count
|
||||||
|
// 0x80 >> right shift count
|
||||||
|
if byt&(0x01<<uint(i)) != 0 {
|
||||||
|
b.bits[b.lenBits/8] |= 0x80 >> uint(b.lenBits%8)
|
||||||
|
}
|
||||||
|
b.lenBits++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendBools append multi bool after the bit stream of b
|
||||||
|
func (b *Binary) AppendBools(booleans ...bool) {
|
||||||
|
b.ensureCapacity(len(booleans))
|
||||||
|
for _, bv := range booleans {
|
||||||
|
if bv {
|
||||||
|
b.bits[b.lenBits/8] |= 0x80 >> uint(b.lenBits%8)
|
||||||
|
}
|
||||||
|
b.lenBits++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendNumBools appends num bits of value value.
|
||||||
|
func (b *Binary) AppendNumBools(num int, boolean bool) {
|
||||||
|
booleans := make([]bool, num)
|
||||||
|
// if not false just append
|
||||||
|
if boolean {
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
booleans[i] = boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.AppendBools(booleans...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IterFunc used by func b.VisitAll ...
|
||||||
|
type IterFunc func(pos int, v bool)
|
||||||
|
|
||||||
|
// VisitAll loop the b.bits stream and send value into IterFunc
|
||||||
|
func (b *Binary) VisitAll(f IterFunc) {
|
||||||
|
for pos := 0; pos < b.Len(); pos++ {
|
||||||
|
f(pos, b.At(pos))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String for printing
|
||||||
|
func (b *Binary) String() string {
|
||||||
|
var (
|
||||||
|
bitstr []byte
|
||||||
|
vb byte
|
||||||
|
)
|
||||||
|
|
||||||
|
b.VisitAll(func(pos int, v bool) {
|
||||||
|
vb = byteFalse
|
||||||
|
if v {
|
||||||
|
vb = byteTrue
|
||||||
|
}
|
||||||
|
bitstr = append(bitstr, vb)
|
||||||
|
})
|
||||||
|
|
||||||
|
return fmt.Sprintf(format, b.Len(), string(bitstr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len ...
|
||||||
|
func (b *Binary) Len() int {
|
||||||
|
return b.lenBits
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes ...
|
||||||
|
func (b *Binary) Bytes() []byte {
|
||||||
|
numBytes := b.lenBits / 8
|
||||||
|
if b.lenBits%8 != 0 {
|
||||||
|
numBytes++
|
||||||
|
}
|
||||||
|
return b.bits[:numBytes]
|
||||||
|
}
|
||||||
|
|
||||||
|
// EqualTo ...
|
||||||
|
func (b *Binary) EqualTo(other *Binary) bool {
|
||||||
|
if b.lenBits != other.lenBits {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
numByte := b.lenBits / 8
|
||||||
|
if !bytes.Equal(b.bits[:numByte], other.bits[:numByte]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for pos := numByte * 8; pos < b.lenBits; pos++ {
|
||||||
|
if b.At(pos) != other.At(pos) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy ...
|
||||||
|
func (b *Binary) Copy() *Binary {
|
||||||
|
return &Binary{
|
||||||
|
bits: b.bits,
|
||||||
|
lenBits: b.lenBits,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package reedsolomon
|
||||||
|
|
||||||
|
// ref to https://en.wikiversity.org/wiki/Reed%E2%80%93Solomon_codes_for_coders
|
||||||
|
// ref to https://www.jianshu.com/p/8208aad537bb
|
||||||
|
// gf.go Galois Fields
|
||||||
|
|
||||||
|
var (
|
||||||
|
gfLog = []byte{}
|
||||||
|
gfExp = []byte{}
|
||||||
|
)
|
||||||
|
|
||||||
|
const prim = 0x011d
|
||||||
|
|
||||||
|
// init calls all initial funcs
|
||||||
|
func init() {
|
||||||
|
initTables()
|
||||||
|
}
|
||||||
|
|
||||||
|
// init gfExp and gfLog array
|
||||||
|
func initTables() {
|
||||||
|
gfExp = make([]byte, 512)
|
||||||
|
gfLog = make([]byte, 256)
|
||||||
|
|
||||||
|
var (
|
||||||
|
x uint16 = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
for i := 0; i < 255; i++ {
|
||||||
|
gfExp[i] = byte(x)
|
||||||
|
gfLog[x] = byte(i)
|
||||||
|
|
||||||
|
x <<= 1
|
||||||
|
// x overflow 256
|
||||||
|
if (x & 0x100) != 0 {
|
||||||
|
x ^= prim
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 255; i < 512; i++ {
|
||||||
|
gfExp[i] = gfExp[i-255]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// multpy
|
||||||
|
func gfMul(x, y byte) byte {
|
||||||
|
if x == 0 || y == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// byte max: 256 but exp cap is 512
|
||||||
|
return gfExp[uint(gfLog[x])+uint(gfLog[y])]
|
||||||
|
}
|
||||||
|
|
||||||
|
// divide
|
||||||
|
// func gfDiv(x, y byte) byte {
|
||||||
|
// if y == 0 {
|
||||||
|
// panic("zero division error")
|
||||||
|
// }
|
||||||
|
// if x == 0 {
|
||||||
|
// return 0
|
||||||
|
// }
|
||||||
|
// return gfExp[(uint(gfLog[x])+255-uint(gfLog[y]))%255]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // inverse
|
||||||
|
// func gfInverse(x byte) byte {
|
||||||
|
// return gfExp[255-uint(gfLog[x])]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // pow
|
||||||
|
// func gfPow(x, power byte) byte {
|
||||||
|
// return gfExp[(gfLog[x]*power)%255]
|
||||||
|
// }
|
|
@ -0,0 +1,88 @@
|
||||||
|
package reedsolomon
|
||||||
|
|
||||||
|
// generator polynomial
|
||||||
|
// (x-a^1) * (x - a^2) * .... * (x -a^numECWords-1)
|
||||||
|
func rsGenPoly(numECWords int) []byte {
|
||||||
|
var generator = []byte{1}
|
||||||
|
for i := 0; i < numECWords; i++ {
|
||||||
|
generator = polyMul(generator, []byte{1, gfExp[i]})
|
||||||
|
}
|
||||||
|
return generator
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将一个多项式和一个标量相乘
|
||||||
|
func polyScale(poly []byte, x byte) []byte {
|
||||||
|
result := make([]byte, len(poly))
|
||||||
|
for i := 0; i < len(poly); i++ {
|
||||||
|
result[i] = gfMul(poly[i], x)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func polyAdd(poly1, poly2 []byte) []byte {
|
||||||
|
size1 := len(poly1)
|
||||||
|
size2 := len(poly2)
|
||||||
|
size := size1
|
||||||
|
|
||||||
|
if size2 > size1 {
|
||||||
|
size = size2
|
||||||
|
}
|
||||||
|
result := make([]byte, size)
|
||||||
|
|
||||||
|
for i := 0; i < size1; i++ {
|
||||||
|
result[i] = byte(poly1[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < size2; i++ {
|
||||||
|
result[i] ^= byte(poly2[i])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// mul polynomial
|
||||||
|
func polyMul(poly1, poly2 []byte) []byte {
|
||||||
|
result := make([]byte, len(poly1)+len(poly2)-1)
|
||||||
|
for i := 0; i < len(poly1); i++ {
|
||||||
|
for j := 0; j < len(poly2); j++ {
|
||||||
|
result[i+j] ^= gfMul(poly1[i], poly2[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// func polyEval(poly []byte, x byte) byte {
|
||||||
|
// y := poly[0]
|
||||||
|
// for i := 1; i < len(poly); i++ {
|
||||||
|
// y = gfMul(y, x) ^ poly[i]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return y
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ref to: https://www.thonky.com/qr-code-tutorial/show-division-steps?msg_coeff=12%2C34%2C56%2C23&num_ecc_blocks=3
|
||||||
|
func polyDiv(dividend, divisor []byte) []byte {
|
||||||
|
if len(dividend) == 0 {
|
||||||
|
panic("could not div with 0 length dividend")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
leadTerm = dividend[0]
|
||||||
|
reminder, a, b []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
reminder = dividend
|
||||||
|
|
||||||
|
for i := 0; i < len(dividend); i++ {
|
||||||
|
// step a: generator * leadTerm
|
||||||
|
a = polyScale(divisor, leadTerm)
|
||||||
|
|
||||||
|
// step b, xor operation
|
||||||
|
b = polyAdd(reminder, a)
|
||||||
|
|
||||||
|
// discard lead term of b
|
||||||
|
reminder = b[1:]
|
||||||
|
leadTerm = reminder[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return reminder
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Package reedsolomon ...
|
||||||
|
// ref to doc: https://en.wikiversity.org/wiki/Reed%E2%80%93Solomon_codes_for_coders#Polynomial_division
|
||||||
|
// ref to project: github.com/skip2/go-qrcode/reedsolomon
|
||||||
|
package reedsolomon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yeqown/reedsolomon/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
type word byte // 8bit as a word
|
||||||
|
|
||||||
|
// Encode ...
|
||||||
|
func Encode(bin *binary.Binary, numECWords int) *binary.Binary {
|
||||||
|
if bin.Len()%8 != 0 {
|
||||||
|
panic("could not deal with binary times 8bits")
|
||||||
|
}
|
||||||
|
// generate polynomial
|
||||||
|
generator := rsGenPoly(numECWords)
|
||||||
|
|
||||||
|
// poly div
|
||||||
|
remainder := polyDiv(bin.Bytes(), generator)
|
||||||
|
|
||||||
|
// append error correction stream
|
||||||
|
bout := bin.Copy()
|
||||||
|
bout.AppendBytes(remainder...)
|
||||||
|
return bout
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode ...
|
||||||
|
// TODO: finish this ~
|
||||||
|
func Decode(bin *binary.Binary, numECWords int) *binary.Binary {
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1045,6 +1045,17 @@ github.com/xeipuuv/gojsonschema
|
||||||
# github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673
|
# github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673
|
||||||
## explicit
|
## explicit
|
||||||
github.com/xrash/smetrics
|
github.com/xrash/smetrics
|
||||||
|
# github.com/yeqown/go-qrcode/v2 v2.2.1
|
||||||
|
## explicit; go 1.18
|
||||||
|
github.com/yeqown/go-qrcode/v2
|
||||||
|
# github.com/yeqown/go-qrcode/writer/standard v1.2.1
|
||||||
|
## explicit; go 1.17
|
||||||
|
github.com/yeqown/go-qrcode/writer/standard
|
||||||
|
github.com/yeqown/go-qrcode/writer/standard/imgkit
|
||||||
|
# github.com/yeqown/reedsolomon v1.0.0
|
||||||
|
## explicit
|
||||||
|
github.com/yeqown/reedsolomon
|
||||||
|
github.com/yeqown/reedsolomon/binary
|
||||||
# github.com/zenthangplus/goccm v0.0.0-20211005163543-2f2e522aca15
|
# github.com/zenthangplus/goccm v0.0.0-20211005163543-2f2e522aca15
|
||||||
## explicit; go 1.14
|
## explicit; go 1.14
|
||||||
github.com/zenthangplus/goccm
|
github.com/zenthangplus/goccm
|
||||||
|
|
Loading…
Reference in New Issue