status-im/status-go

View on GitHub
server/qrops.go

Summary

Maintainability
A
0 mins
Test Coverage
D
60%
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)
    }
    circle := images.CreateCircleWithPadding(img, padding)
    resultBytes, err := images.EncodePNG(circle)
    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
    }

    // default padding to 10 to make the QR with profile image look as per
    // the designs
    padding = 10

    if identityImageObjectFromDB == nil {
        LogoBytes, err = ToLogoImageFromBytes(staticImageData, padding)
    } else {
        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

}