github.com/status-im/status-go@v1.1.0/server/qrops.go (about)

     1  package server
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"errors"
     7  	"fmt"
     8  	"image"
     9  	"net/url"
    10  	"strconv"
    11  
    12  	"github.com/yeqown/go-qrcode/v2"
    13  	"github.com/yeqown/go-qrcode/writer/standard"
    14  	"go.uber.org/zap"
    15  
    16  	"github.com/status-im/status-go/images"
    17  	"github.com/status-im/status-go/multiaccounts"
    18  )
    19  
    20  type WriterCloserByteBuffer struct {
    21  	*bytes.Buffer
    22  }
    23  
    24  func (wc WriterCloserByteBuffer) Close() error {
    25  	return nil
    26  }
    27  
    28  func NewWriterCloserByteBuffer() *WriterCloserByteBuffer {
    29  	return &WriterCloserByteBuffer{bytes.NewBuffer([]byte{})}
    30  }
    31  
    32  type QRConfig struct {
    33  	DecodedQRURL    string
    34  	WithLogo        bool
    35  	CorrectionLevel qrcode.EncodeOption
    36  	KeyUID          string
    37  	ImageName       string
    38  	Size            int
    39  	Params          url.Values
    40  }
    41  
    42  func NewQRConfig(params url.Values, logger *zap.Logger) (*QRConfig, error) {
    43  	config := &QRConfig{}
    44  	config.Params = params
    45  	err := config.setQrURL()
    46  
    47  	if err != nil {
    48  		logger.Error("[qrops-error] error in setting QRURL", zap.Error(err))
    49  		return nil, err
    50  	}
    51  
    52  	config.setAllowProfileImage()
    53  	config.setErrorCorrectionLevel()
    54  	err = config.setSize()
    55  
    56  	if err != nil {
    57  		logger.Error("[qrops-error] could not convert string to int for size param ", zap.Error(err))
    58  		return nil, err
    59  	}
    60  
    61  	if config.WithLogo {
    62  		err = config.setKeyUID()
    63  
    64  		if err != nil {
    65  			logger.Error(err.Error())
    66  			return nil, err
    67  		}
    68  
    69  		config.setImageName()
    70  	}
    71  
    72  	return config, nil
    73  }
    74  
    75  func (q *QRConfig) setQrURL() error {
    76  	qrURL, ok := q.Params["url"]
    77  
    78  	if !ok || len(qrURL) == 0 {
    79  		return errors.New("[qrops-error] no qr url provided")
    80  	}
    81  
    82  	decodedURL, err := base64.StdEncoding.DecodeString(qrURL[0])
    83  
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	q.DecodedQRURL = string(decodedURL)
    89  	return nil
    90  }
    91  
    92  func (q *QRConfig) setAllowProfileImage() {
    93  	allowProfileImage, ok := q.Params["allowProfileImage"]
    94  
    95  	if !ok || len(allowProfileImage) == 0 {
    96  		// we default to false when this flag was not provided
    97  		// so someone does not want to allowProfileImage on their QR Image
    98  		// fine then :)
    99  		q.WithLogo = false
   100  	}
   101  
   102  	LogoOnImage, err := strconv.ParseBool(allowProfileImage[0])
   103  
   104  	if err != nil {
   105  		// maybe for fun someone tries to send non-boolean values to this flag
   106  		// we also default to false in that case
   107  		q.WithLogo = false
   108  	}
   109  
   110  	// if we reach here its most probably true
   111  	q.WithLogo = LogoOnImage
   112  }
   113  
   114  func (q *QRConfig) setErrorCorrectionLevel() {
   115  	level, ok := q.Params["level"]
   116  	if !ok || len(level) == 0 {
   117  		// we default to MediumLevel of error correction when the level flag
   118  		// is not passed.
   119  		q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
   120  	}
   121  
   122  	levelInt, err := strconv.Atoi(level[0])
   123  	if err != nil || levelInt < 0 {
   124  		// if there is any issue with string to int conversion
   125  		// we still default to MediumLevel of error correction
   126  		q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
   127  	}
   128  
   129  	switch levelInt {
   130  	case 1:
   131  		q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionLow)
   132  	case 2:
   133  		q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
   134  	case 3:
   135  		q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionQuart)
   136  	case 4:
   137  		q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionHighest)
   138  	default:
   139  		q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
   140  	}
   141  }
   142  
   143  func (q *QRConfig) setSize() error {
   144  	size, ok := q.Params["size"]
   145  
   146  	if ok {
   147  		imageToBeResized, err := strconv.Atoi(size[0])
   148  
   149  		if err != nil {
   150  			return err
   151  		}
   152  
   153  		if imageToBeResized <= 0 {
   154  			return errors.New("[qrops-error] Got an invalid size parameter, it should be greater than zero")
   155  		}
   156  
   157  		q.Size = imageToBeResized
   158  
   159  	}
   160  
   161  	return nil
   162  }
   163  
   164  func (q *QRConfig) setKeyUID() error {
   165  	keyUID, ok := q.Params["keyUid"]
   166  	// the keyUID was not passed, which is a requirement to get the multiaccount image,
   167  	// so we log this error
   168  	if !ok || len(keyUID) == 0 {
   169  		return errors.New("[qrops-error] A keyUID is required to put logo on image and it was not passed in the parameters")
   170  	}
   171  
   172  	q.KeyUID = keyUID[0]
   173  	return nil
   174  }
   175  
   176  func (q *QRConfig) setImageName() {
   177  	imageName, ok := q.Params["imageName"]
   178  	//if the imageName was not passed, we default to const images.LargeDimName
   179  	if !ok || len(imageName) == 0 {
   180  		q.ImageName = images.LargeDimName
   181  	}
   182  
   183  	q.ImageName = imageName[0]
   184  }
   185  
   186  func ToLogoImageFromBytes(imageBytes []byte, padding int) ([]byte, error) {
   187  	img, _, err := image.Decode(bytes.NewReader(imageBytes))
   188  	if err != nil {
   189  		return nil, fmt.Errorf("decoding image failed: %v", err)
   190  	}
   191  	circle := images.CreateCircleWithPadding(img, padding)
   192  	resultBytes, err := images.EncodePNG(circle)
   193  	if err != nil {
   194  		return nil, fmt.Errorf("encoding PNG failed: %v", err)
   195  	}
   196  	return resultBytes, nil
   197  }
   198  
   199  func GetLogoImage(multiaccountsDB *multiaccounts.Database, keyUID string, imageName string) ([]byte, error) {
   200  	var (
   201  		padding   int
   202  		LogoBytes []byte
   203  	)
   204  
   205  	staticImageData, err := images.Asset("_assets/tests/qr/status.png")
   206  	if err != nil { // Asset was not found.
   207  		return nil, err
   208  	}
   209  	identityImageObjectFromDB, err := multiaccountsDB.GetIdentityImage(keyUID, imageName)
   210  
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  
   215  	// default padding to 10 to make the QR with profile image look as per
   216  	// the designs
   217  	padding = 10
   218  
   219  	if identityImageObjectFromDB == nil {
   220  		LogoBytes, err = ToLogoImageFromBytes(staticImageData, padding)
   221  	} else {
   222  		LogoBytes, err = ToLogoImageFromBytes(identityImageObjectFromDB.Payload, padding)
   223  	}
   224  
   225  	return LogoBytes, err
   226  }
   227  
   228  func GetPadding(imgBytes []byte) int {
   229  	const (
   230  		defaultPadding = 20
   231  	)
   232  	size, _, err := images.GetImageDimensions(imgBytes)
   233  	if err != nil {
   234  		return defaultPadding
   235  	}
   236  	return size / 5
   237  }
   238  
   239  func generateQRBytes(params url.Values, logger *zap.Logger, multiaccountsDB *multiaccounts.Database) []byte {
   240  
   241  	qrGenerationConfig, err := NewQRConfig(params, logger)
   242  
   243  	if err != nil {
   244  		logger.Error("could not generate QRConfig please rectify the errors with input parameters", zap.Error(err))
   245  		return nil
   246  	}
   247  
   248  	qrc, err := qrcode.NewWith(qrGenerationConfig.DecodedQRURL,
   249  		qrcode.WithEncodingMode(qrcode.EncModeAuto),
   250  		qrGenerationConfig.CorrectionLevel,
   251  	)
   252  
   253  	if err != nil {
   254  		logger.Error("could not generate QRCode with provided options", zap.Error(err))
   255  		return nil
   256  	}
   257  
   258  	buf := NewWriterCloserByteBuffer()
   259  	nw := standard.NewWithWriter(buf)
   260  	err = qrc.Save(nw)
   261  
   262  	if err != nil {
   263  		logger.Error("could not save image", zap.Error(err))
   264  		return nil
   265  	}
   266  
   267  	payload := buf.Bytes()
   268  
   269  	if qrGenerationConfig.WithLogo {
   270  		logo, err := GetLogoImage(multiaccountsDB, qrGenerationConfig.KeyUID, qrGenerationConfig.ImageName)
   271  
   272  		if err != nil {
   273  			logger.Error("could not get logo image from multiaccountsDB", zap.Error(err))
   274  			return nil
   275  		}
   276  
   277  		qrWidth, qrHeight, err := images.GetImageDimensions(payload)
   278  
   279  		if err != nil {
   280  			logger.Error("could not get image dimensions from payload", zap.Error(err))
   281  			return nil
   282  		}
   283  
   284  		logo, err = images.ResizeImage(logo, qrWidth/5, qrHeight/5)
   285  
   286  		if err != nil {
   287  			logger.Error("could not resize logo image ", zap.Error(err))
   288  			return nil
   289  		}
   290  
   291  		payload = images.SuperimposeLogoOnQRImage(payload, logo)
   292  	}
   293  
   294  	if qrGenerationConfig.Size > 0 {
   295  
   296  		payload, err = images.ResizeImage(payload, qrGenerationConfig.Size, qrGenerationConfig.Size)
   297  
   298  		if err != nil {
   299  			logger.Error("could not resize final logo image ", zap.Error(err))
   300  			return nil
   301  		}
   302  	}
   303  
   304  	return payload
   305  
   306  }