307 lines
7.3 KiB
Go
307 lines
7.3 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"net/url"
|
|
"strconv"
|
|
|
|
"github.com/yeqown/go-qrcode/v2"
|
|
"github.com/yeqown/go-qrcode/writer/standard"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/status-im/status-go/images"
|
|
"github.com/status-im/status-go/multiaccounts"
|
|
)
|
|
|
|
type WriterCloserByteBuffer struct {
|
|
*bytes.Buffer
|
|
}
|
|
|
|
func (wc WriterCloserByteBuffer) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func NewWriterCloserByteBuffer() *WriterCloserByteBuffer {
|
|
return &WriterCloserByteBuffer{bytes.NewBuffer([]byte{})}
|
|
}
|
|
|
|
type QRConfig struct {
|
|
DecodedQRURL string
|
|
WithLogo bool
|
|
CorrectionLevel qrcode.EncodeOption
|
|
KeyUID string
|
|
ImageName string
|
|
Size int
|
|
Params url.Values
|
|
}
|
|
|
|
func NewQRConfig(params url.Values, logger *zap.Logger) (*QRConfig, error) {
|
|
config := &QRConfig{}
|
|
config.Params = params
|
|
err := config.setQrURL()
|
|
|
|
if err != nil {
|
|
logger.Error("[qrops-error] error in setting QRURL", zap.Error(err))
|
|
return nil, err
|
|
}
|
|
|
|
config.setAllowProfileImage()
|
|
config.setErrorCorrectionLevel()
|
|
err = config.setSize()
|
|
|
|
if err != nil {
|
|
logger.Error("[qrops-error] could not convert string to int for size param ", zap.Error(err))
|
|
return nil, err
|
|
}
|
|
|
|
if config.WithLogo {
|
|
err = config.setKeyUID()
|
|
|
|
if err != nil {
|
|
logger.Error(err.Error())
|
|
return nil, err
|
|
}
|
|
|
|
config.setImageName()
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
func (q *QRConfig) setQrURL() error {
|
|
qrURL, ok := q.Params["url"]
|
|
|
|
if !ok || len(qrURL) == 0 {
|
|
return errors.New("[qrops-error] no qr url provided")
|
|
}
|
|
|
|
decodedURL, err := base64.StdEncoding.DecodeString(qrURL[0])
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
q.DecodedQRURL = string(decodedURL)
|
|
return nil
|
|
}
|
|
|
|
func (q *QRConfig) setAllowProfileImage() {
|
|
allowProfileImage, ok := q.Params["allowProfileImage"]
|
|
|
|
if !ok || len(allowProfileImage) == 0 {
|
|
// we default to false when this flag was not provided
|
|
// so someone does not want to allowProfileImage on their QR Image
|
|
// fine then :)
|
|
q.WithLogo = false
|
|
}
|
|
|
|
LogoOnImage, err := strconv.ParseBool(allowProfileImage[0])
|
|
|
|
if err != nil {
|
|
// maybe for fun someone tries to send non-boolean values to this flag
|
|
// we also default to false in that case
|
|
q.WithLogo = false
|
|
}
|
|
|
|
// if we reach here its most probably true
|
|
q.WithLogo = LogoOnImage
|
|
}
|
|
|
|
func (q *QRConfig) setErrorCorrectionLevel() {
|
|
level, ok := q.Params["level"]
|
|
if !ok || len(level) == 0 {
|
|
// we default to MediumLevel of error correction when the level flag
|
|
// is not passed.
|
|
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
|
|
}
|
|
|
|
levelInt, err := strconv.Atoi(level[0])
|
|
if err != nil || levelInt < 0 {
|
|
// if there is any issue with string to int conversion
|
|
// we still default to MediumLevel of error correction
|
|
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
|
|
}
|
|
|
|
switch levelInt {
|
|
case 1:
|
|
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionLow)
|
|
case 2:
|
|
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
|
|
case 3:
|
|
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionQuart)
|
|
case 4:
|
|
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionHighest)
|
|
default:
|
|
q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
|
|
}
|
|
}
|
|
|
|
func (q *QRConfig) setSize() error {
|
|
size, ok := q.Params["size"]
|
|
|
|
if ok {
|
|
imageToBeResized, err := strconv.Atoi(size[0])
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if imageToBeResized <= 0 {
|
|
return errors.New("[qrops-error] Got an invalid size parameter, it should be greater than zero")
|
|
}
|
|
|
|
q.Size = imageToBeResized
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (q *QRConfig) setKeyUID() error {
|
|
keyUID, ok := q.Params["keyUid"]
|
|
// the keyUID was not passed, which is a requirement to get the multiaccount image,
|
|
// so we log this error
|
|
if !ok || len(keyUID) == 0 {
|
|
return errors.New("[qrops-error] A keyUID is required to put logo on image and it was not passed in the parameters")
|
|
}
|
|
|
|
q.KeyUID = keyUID[0]
|
|
return nil
|
|
}
|
|
|
|
func (q *QRConfig) setImageName() {
|
|
imageName, ok := q.Params["imageName"]
|
|
//if the imageName was not passed, we default to const images.LargeDimName
|
|
if !ok || len(imageName) == 0 {
|
|
q.ImageName = images.LargeDimName
|
|
}
|
|
|
|
q.ImageName = imageName[0]
|
|
}
|
|
|
|
func ToLogoImageFromBytes(imageBytes []byte, padding int) ([]byte, error) {
|
|
img, _, err := image.Decode(bytes.NewReader(imageBytes))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decoding image failed: %v", err)
|
|
}
|
|
paddedImg := images.AddPadding(img, padding)
|
|
circle := images.CreateCircle(img)
|
|
centeredImg := images.PlaceCircleInCenter(paddedImg, circle)
|
|
resultBytes, err := images.EncodePNG(centeredImg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encoding PNG failed: %v", err)
|
|
}
|
|
return resultBytes, nil
|
|
}
|
|
|
|
func GetLogoImage(multiaccountsDB *multiaccounts.Database, keyUID string, imageName string) ([]byte, error) {
|
|
var (
|
|
padding int
|
|
LogoBytes []byte
|
|
)
|
|
|
|
staticImageData, err := images.Asset("_assets/tests/qr/status.png")
|
|
if err != nil { // Asset was not found.
|
|
return nil, err
|
|
}
|
|
identityImageObjectFromDB, err := multiaccountsDB.GetIdentityImage(keyUID, imageName)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if identityImageObjectFromDB == nil {
|
|
padding = GetPadding(staticImageData)
|
|
LogoBytes, err = ToLogoImageFromBytes(staticImageData, padding)
|
|
} else {
|
|
padding = GetPadding(identityImageObjectFromDB.Payload)
|
|
LogoBytes, err = ToLogoImageFromBytes(identityImageObjectFromDB.Payload, padding)
|
|
}
|
|
|
|
return LogoBytes, err
|
|
}
|
|
|
|
func GetPadding(imgBytes []byte) int {
|
|
const (
|
|
defaultPadding = 20
|
|
)
|
|
size, _, err := images.GetImageDimensions(imgBytes)
|
|
if err != nil {
|
|
return defaultPadding
|
|
}
|
|
return size / 5
|
|
}
|
|
|
|
func generateQRBytes(params url.Values, logger *zap.Logger, multiaccountsDB *multiaccounts.Database) []byte {
|
|
|
|
qrGenerationConfig, err := NewQRConfig(params, logger)
|
|
|
|
if err != nil {
|
|
logger.Error("could not generate QRConfig please rectify the errors with input parameters", zap.Error(err))
|
|
return nil
|
|
}
|
|
|
|
qrc, err := qrcode.NewWith(qrGenerationConfig.DecodedQRURL,
|
|
qrcode.WithEncodingMode(qrcode.EncModeAuto),
|
|
qrGenerationConfig.CorrectionLevel,
|
|
)
|
|
|
|
if err != nil {
|
|
logger.Error("could not generate QRCode with provided options", zap.Error(err))
|
|
return nil
|
|
}
|
|
|
|
buf := NewWriterCloserByteBuffer()
|
|
nw := standard.NewWithWriter(buf)
|
|
err = qrc.Save(nw)
|
|
|
|
if err != nil {
|
|
logger.Error("could not save image", zap.Error(err))
|
|
return nil
|
|
}
|
|
|
|
payload := buf.Bytes()
|
|
|
|
if qrGenerationConfig.WithLogo {
|
|
logo, err := GetLogoImage(multiaccountsDB, qrGenerationConfig.KeyUID, qrGenerationConfig.ImageName)
|
|
|
|
if err != nil {
|
|
logger.Error("could not get logo image from multiaccountsDB", zap.Error(err))
|
|
return nil
|
|
}
|
|
|
|
qrWidth, qrHeight, err := images.GetImageDimensions(payload)
|
|
|
|
if err != nil {
|
|
logger.Error("could not get image dimensions from payload", zap.Error(err))
|
|
return nil
|
|
}
|
|
|
|
logo, err = images.ResizeImage(logo, qrWidth/5, qrHeight/5)
|
|
|
|
if err != nil {
|
|
logger.Error("could not resize logo image ", zap.Error(err))
|
|
return nil
|
|
}
|
|
|
|
payload = images.SuperimposeLogoOnQRImage(payload, logo)
|
|
}
|
|
|
|
if qrGenerationConfig.Size > 0 {
|
|
|
|
payload, err = images.ResizeImage(payload, qrGenerationConfig.Size, qrGenerationConfig.Size)
|
|
|
|
if err != nil {
|
|
logger.Error("could not resize final logo image ", zap.Error(err))
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return payload
|
|
|
|
}
|