github.com/status-im/status-go@v1.1.0/images/manipulation.go (about) 1 package images 2 3 import ( 4 "bytes" 5 "fmt" 6 "image" 7 "image/color" 8 "image/draw" 9 "image/png" 10 "math" 11 "os" 12 13 "github.com/nfnt/resize" 14 "github.com/oliamb/cutter" 15 "go.uber.org/zap" 16 xdraw "golang.org/x/image/draw" 17 18 "github.com/ethereum/go-ethereum/log" 19 ) 20 21 type Circle struct { 22 X, Y, R int 23 } 24 25 func (c *Circle) ColorModel() color.Model { 26 return color.AlphaModel 27 } 28 func (c *Circle) Bounds() image.Rectangle { 29 return image.Rect(c.X-c.R, c.Y-c.R, c.X+c.R, c.Y+c.R) 30 } 31 func (c *Circle) At(x, y int) color.Color { 32 xx, yy, rr := float64(x-c.X)+0.5, float64(y-c.Y)+0.5, float64(c.R) 33 if xx*xx+yy*yy < rr*rr { 34 return color.Alpha{255} 35 } 36 return color.Alpha{0} 37 } 38 39 func Resize(size ResizeDimension, img image.Image) image.Image { 40 var width, height uint 41 42 switch { 43 case img.Bounds().Max.X == img.Bounds().Max.Y: 44 width, height = uint(size), uint(size) 45 case img.Bounds().Max.X > img.Bounds().Max.Y: 46 width, height = 0, uint(size) 47 default: 48 width, height = uint(size), 0 49 } 50 51 log.Info("resizing", "size", size, "width", width, "height", height) 52 53 return resize.Resize(width, height, img, resize.Bilinear) 54 } 55 56 func ResizeTo(percent int, img image.Image) image.Image { 57 width := uint(img.Bounds().Max.X * percent / 100) 58 height := uint(img.Bounds().Max.Y * percent / 100) 59 60 return resize.Resize(width, height, img, resize.Bilinear) 61 } 62 63 func ShrinkOnly(size ResizeDimension, img image.Image) image.Image { 64 finalSize := int(math.Min(float64(size), math.Min(float64(img.Bounds().Dx()), float64(img.Bounds().Dy())))) 65 return Resize(ResizeDimension(finalSize), img) 66 } 67 68 func Crop(img image.Image, rect image.Rectangle) (image.Image, error) { 69 70 if img.Bounds().Max.X < rect.Max.X || img.Bounds().Max.Y < rect.Max.Y { 71 return nil, fmt.Errorf( 72 "crop dimensions out of bounds of image, image width '%dpx' & height '%dpx'; crop bottom right coordinate at X '%dpx' Y '%dpx'", 73 img.Bounds().Max.X, img.Bounds().Max.Y, 74 rect.Max.X, rect.Max.Y, 75 ) 76 } 77 78 return cutter.Crop(img, cutter.Config{ 79 Width: rect.Dx(), 80 Height: rect.Dy(), 81 Anchor: rect.Min, 82 }) 83 } 84 85 // CropCenter takes an image, usually downloaded from a URL 86 // If the image is square, the full image is returned 87 // If the image is rectangular, the largest central square is returned 88 // calculations at _docs/image-center-crop-calculations.png 89 func CropCenter(img image.Image) (image.Image, error) { 90 var cropRect image.Rectangle 91 maxBounds := img.Bounds().Max 92 93 if maxBounds.X == maxBounds.Y { 94 return img, nil 95 } 96 97 if maxBounds.X > maxBounds.Y { 98 // the final output should be YxY 99 cropRect = image.Rectangle{ 100 Min: image.Point{X: maxBounds.X/2 - maxBounds.Y/2, Y: 0}, 101 Max: image.Point{X: maxBounds.X/2 + maxBounds.Y/2, Y: maxBounds.Y}, 102 } 103 } else { 104 // the final output should be XxX 105 cropRect = image.Rectangle{ 106 Min: image.Point{X: 0, Y: maxBounds.Y/2 - maxBounds.X/2}, 107 Max: image.Point{X: maxBounds.X, Y: maxBounds.Y/2 + maxBounds.X/2}, 108 } 109 } 110 return Crop(img, cropRect) 111 } 112 113 func ImageToBytes(imagePath string) ([]byte, error) { 114 // Open the image file 115 file, err := os.Open(imagePath) 116 if err != nil { 117 return nil, err 118 } 119 defer file.Close() 120 121 // Decode the image 122 img, _, err := image.Decode(file) 123 if err != nil { 124 return nil, err 125 } 126 127 // Create a new buffer to hold the image data 128 var imgBuffer bytes.Buffer 129 130 // Encode the image to the desired format and save it in the buffer 131 err = png.Encode(&imgBuffer, img) 132 if err != nil { 133 return nil, err 134 } 135 136 // Return the image data as a byte slice 137 return imgBuffer.Bytes(), nil 138 } 139 140 func ImageToBytesAndImage(imagePath string) ([]byte, image.Image, error) { 141 // Open the image file 142 file, err := os.Open(imagePath) 143 if err != nil { 144 return nil, nil, err 145 } 146 defer file.Close() 147 148 // Decode the image 149 img, _, err := image.Decode(file) 150 if err != nil { 151 return nil, nil, err 152 } 153 154 // Create a new buffer to hold the image data 155 var imgBuffer bytes.Buffer 156 157 // Encode the image to the desired format and save it in the buffer 158 err = png.Encode(&imgBuffer, img) 159 if err != nil { 160 return nil, nil, err 161 } 162 163 // Return the image data as a byte slice 164 return imgBuffer.Bytes(), img, nil 165 } 166 167 func AddPadding(img image.Image, padding int) *image.RGBA { 168 bounds := img.Bounds() 169 newBounds := image.Rect(bounds.Min.X-padding, bounds.Min.Y-padding, bounds.Max.X+padding, bounds.Max.Y+padding) 170 paddedImg := image.NewRGBA(newBounds) 171 draw.Draw(paddedImg, newBounds, &image.Uniform{C: color.White}, image.ZP, draw.Src) 172 173 return paddedImg 174 } 175 176 func EncodePNG(img *image.RGBA) ([]byte, error) { 177 resultImg := &bytes.Buffer{} 178 err := png.Encode(resultImg, img) 179 if err != nil { 180 return nil, err 181 } 182 return resultImg.Bytes(), nil 183 } 184 185 func CreateCircleWithPadding(img image.Image, padding int) *image.RGBA { 186 bounds := img.Bounds() 187 width := bounds.Dx() 188 // only relying on width as a metric here because we know that we 189 // store profile images in a perfect circle 190 radius := width / 2 191 192 paddedWidth := width + 2*padding 193 paddedRadius := paddedWidth / 2 194 195 // Create a new circular image with padding 196 newBounds := image.Rect(0, 0, paddedWidth, paddedWidth) 197 circle := image.NewRGBA(newBounds) 198 199 // Create a larger circular mask for the padding 200 paddingMask := &Circle{ 201 X: paddedRadius, 202 Y: paddedRadius, 203 R: paddedRadius, 204 } 205 206 // Draw the white color onto the circle with padding mask 207 draw.DrawMask(circle, circle.Bounds(), image.NewUniform(color.White), image.ZP, paddingMask, image.ZP, draw.Src) 208 209 // Create a new circle mask with the original size 210 circleMask := &Circle{ 211 X: radius, 212 Y: radius, 213 R: radius, 214 } 215 216 // Draw the original image onto the white circular image at the center (with padding offset) 217 draw.DrawMask(circle, bounds.Add(image.Pt(padding, padding)), img, image.ZP, circleMask, image.ZP, draw.Over) 218 219 return circle 220 } 221 222 func RoundCrop(inputImage []byte) ([]byte, error) { 223 img, _, err := image.Decode(bytes.NewReader(inputImage)) 224 if err != nil { 225 return nil, err 226 } 227 result := CreateCircleWithPadding(img, 0) 228 229 var outputImage bytes.Buffer 230 err = png.Encode(&outputImage, result) 231 if err != nil { 232 return nil, err 233 } 234 return outputImage.Bytes(), nil 235 } 236 237 func PlaceCircleInCenter(paddedImg, circle *image.RGBA) *image.RGBA { 238 bounds := circle.Bounds() 239 centerX := (paddedImg.Bounds().Min.X + paddedImg.Bounds().Max.X) / 2 240 centerY := (paddedImg.Bounds().Min.Y + paddedImg.Bounds().Max.Y) / 2 241 draw.Draw(paddedImg, bounds.Add(image.Pt(centerX-bounds.Dx()/2, centerY-bounds.Dy()/2)), circle, image.ZP, draw.Over) 242 return paddedImg 243 } 244 245 func ResizeImage(imgBytes []byte, width, height int) ([]byte, error) { 246 // Decode image bytes 247 img, _, err := image.Decode(bytes.NewReader(imgBytes)) 248 if err != nil { 249 return nil, err 250 } 251 // Create a new image with the desired dimensions 252 newImg := image.NewNRGBA(image.Rect(0, 0, width, height)) 253 xdraw.BiLinear.Scale(newImg, newImg.Bounds(), img, img.Bounds(), draw.Over, nil) 254 // Encode the new image to bytes 255 var newImgBytes bytes.Buffer 256 if err = png.Encode(&newImgBytes, newImg); err != nil { 257 return nil, err 258 } 259 return newImgBytes.Bytes(), nil 260 } 261 262 func SuperimposeLogoOnQRImage(imageBytes []byte, qrFilepath []byte) []byte { 263 // Read the two images from bytes 264 img1, _, err := image.Decode(bytes.NewReader(imageBytes)) 265 266 if err != nil { 267 log.Error("error decoding logo Image", zap.Error(err)) 268 return nil 269 } 270 271 img2, _, err := image.Decode(bytes.NewReader(qrFilepath)) 272 273 if err != nil { 274 log.Error("error decoding QR Image", zap.Error(err)) 275 return nil 276 } 277 // Create a new image with the dimensions of the first image 278 result := image.NewRGBA(img1.Bounds()) 279 // Draw the first image on the new image 280 draw.Draw(result, img1.Bounds(), img1, image.ZP, draw.Src) 281 // Get the dimensions of the second image 282 img2Bounds := img2.Bounds() 283 // Calculate the x and y coordinates to center the second image 284 x := (img1.Bounds().Dx() - img2Bounds.Dx()) / 2 285 y := (img1.Bounds().Dy() - img2Bounds.Dy()) / 2 286 // Draw the second image on top of the first image at the calculated coordinates 287 draw.Draw(result, img2Bounds.Add(image.Pt(x, y)), img2, image.ZP, draw.Over) 288 // Encode the final image to a desired format 289 var b bytes.Buffer 290 err = png.Encode(&b, result) 291 292 if err != nil { 293 log.Error("error encoding final result Image to Buffer", zap.Error(err)) 294 return nil 295 } 296 297 return b.Bytes() 298 }