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  }