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 }