🖼 Set any url as profile picture (useful for NFTs) (#2367)

* Sound check
* Add DecodeImageURL fn
* Introduce crop dynamics
* Add center crop calculations png

* Apply suggestions from code review
Co-authored-by: RichΛrd <info@richardramos.me>

* Fix lint
* Rebase and update version
Co-authored-by: RichΛrd <info@richardramos.me>
This commit is contained in:
Shivek Khurana 2021-09-21 14:30:44 +05:30 committed by GitHub
parent 95dcbef5e5
commit a6e7ff6ddd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 168 additions and 16 deletions

View File

@ -1 +1 @@
0.87.1 0.87.2

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 MiB

View File

@ -1,15 +1,21 @@
package images package images
import ( import (
"bytes"
"errors" "errors"
"image" "image"
"image/gif" "image/gif"
"image/jpeg" "image/jpeg"
"image/png" "image/png"
"io" "io"
"io/ioutil"
"net/http"
"os" "os"
"time"
"golang.org/x/image/webp" "golang.org/x/image/webp"
"github.com/ethereum/go-ethereum/log"
) )
func Decode(fileName string) (image.Image, error) { func Decode(fileName string) (image.Image, error) {
@ -27,6 +33,29 @@ func Decode(fileName string) (image.Image, error) {
return decodeImageData(fb, file) return decodeImageData(fb, file)
} }
func DecodeFromURL(path string) (image.Image, error) {
client := http.Client{
Timeout: 5 * time.Second,
}
res, err := client.Get(path)
if err != nil {
return nil, err
}
defer func() {
if err := res.Body.Close(); err != nil {
log.Error("failed to close profile pic http request body", "err", err)
}
}()
bodyBytes, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
return decodeImageData(bodyBytes, bytes.NewReader(bodyBytes))
}
func prepareFileForDecode(file *os.File) ([]byte, error) { func prepareFileForDecode(file *os.File) ([]byte, error) {
// Read the first 14 bytes, used for performing image type checks before parsing the image data // Read the first 14 bytes, used for performing image type checks before parsing the image data
fb := make([]byte, 14) fb := make([]byte, 14)

View File

@ -84,6 +84,67 @@ func TestDecode(t *testing.T) {
} }
} }
func TestDecodeFromURL(t *testing.T) {
cs := []struct {
Filepath string
Nil bool
Bounds image.Rectangle
}{
{
"https://via.placeholder.com/2x1.png",
false,
image.Rectangle{
Min: image.Point{X: 0, Y: 0},
Max: image.Point{X: 2, Y: 1},
},
},
{
"https://via.placeholder.com/1.jpg",
false,
image.Rectangle{
Min: image.Point{X: 0, Y: 0},
Max: image.Point{X: 1, Y: 1},
},
},
{
"https://via.placeholder.com/1.gif",
false,
image.Rectangle{
Min: image.Point{X: 0, Y: 0},
Max: image.Point{X: 1, Y: 1},
},
},
{
"https://via.placeholder.com/1.webp",
false,
image.Rectangle{
Min: image.Point{X: 0, Y: 0},
Max: image.Point{X: 1, Y: 1},
},
},
{
"https://via.placeholder.com/1.webp",
true,
image.Rectangle{
Min: image.Point{X: 0, Y: 0},
Max: image.Point{X: 10, Y: 10},
},
},
}
for _, c := range cs {
img, err := DecodeFromURL(c.Filepath)
if c.Nil {
require.Nil(t, err)
} else {
require.NoError(t, err)
require.Exactly(t, c.Bounds, img.Bounds())
}
}
}
func TestGetType(t *testing.T) { func TestGetType(t *testing.T) {
cs := []struct { cs := []struct {
Buf []byte Buf []byte

View File

@ -5,22 +5,10 @@ import (
"image" "image"
) )
func GenerateIdentityImages(filepath string, aX, aY, bX, bY int) ([]*IdentityImage, error) { func GenerateImageVariants(cImg image.Image) ([]*IdentityImage, error) {
img, err := Decode(filepath)
if err != nil {
return nil, err
}
cropRect := image.Rectangle{
Min: image.Point{X: aX, Y: aY},
Max: image.Point{X: bX, Y: bY},
}
cImg, err := Crop(img, cropRect)
if err != nil {
return nil, err
}
var iis []*IdentityImage var iis []*IdentityImage
var err error
for _, s := range ResizeDimensions { for _, s := range ResizeDimensions {
rImg := Resize(s, cImg) rImg := Resize(s, cImg)
@ -44,3 +32,35 @@ func GenerateIdentityImages(filepath string, aX, aY, bX, bY int) ([]*IdentityIma
return iis, nil return iis, nil
} }
func GenerateIdentityImages(filepath string, aX, aY, bX, bY int) ([]*IdentityImage, error) {
img, err := Decode(filepath)
if err != nil {
return nil, err
}
cropRect := image.Rectangle{
Min: image.Point{X: aX, Y: aY},
Max: image.Point{X: bX, Y: bY},
}
cImg, err := Crop(img, cropRect)
if err != nil {
return nil, err
}
return GenerateImageVariants(cImg)
}
func GenerateIdentityImagesFromURL(url string) ([]*IdentityImage, error) {
img, err := DecodeFromURL(url)
if err != nil {
return nil, err
}
cImg, err := CropCenter(img)
if err != nil {
return nil, err
}
return GenerateImageVariants(cImg)
}

View File

@ -43,3 +43,31 @@ func Crop(img image.Image, rect image.Rectangle) (image.Image, error) {
Anchor: rect.Min, Anchor: rect.Min,
}) })
} }
// CropImage takes an image, usually downloaded from a URL
// If the image is square, the full image is returned
// It the image is rectangular, the largest central square is returned
// calculations at _docs/image-center-crop-calculations.png
func CropCenter(img image.Image) (image.Image, error) {
var cropRect image.Rectangle
maxBounds := img.Bounds().Max
if maxBounds.X == maxBounds.Y {
return img, nil
}
if maxBounds.X > maxBounds.Y {
// the final output should be YxY
cropRect = image.Rectangle{
Min: image.Point{X: maxBounds.X/2 - maxBounds.Y/2, Y: 0},
Max: image.Point{X: maxBounds.X/2 + maxBounds.Y/2, Y: maxBounds.Y},
}
} else {
// the final output should be XxX
cropRect = image.Rectangle{
Min: image.Point{X: 0, Y: maxBounds.Y/2 - maxBounds.X/2},
Max: image.Point{X: maxBounds.X, Y: maxBounds.Y/2 + maxBounds.X/2},
}
}
return Crop(img, cropRect)
}

View File

@ -59,6 +59,20 @@ func (api *MultiAccountsAPI) StoreIdentityImage(keyUID, filepath string, aX, aY,
return iis, err return iis, err
} }
func (api *MultiAccountsAPI) StoreIdentityImageFromURL(keyUID, url string) ([]*images.IdentityImage, error) {
iis, err := images.GenerateIdentityImagesFromURL(url)
if err != nil {
return nil, err
}
err = api.db.StoreIdentityImages(keyUID, iis)
if err != nil {
return nil, err
}
return iis, err
}
// DeleteIdentityImage deletes an IdentityImage from the db with the given name // DeleteIdentityImage deletes an IdentityImage from the db with the given name
func (api *MultiAccountsAPI) DeleteIdentityImage(keyUID string) error { func (api *MultiAccountsAPI) DeleteIdentityImage(keyUID string) error {
return api.db.DeleteIdentityImage(keyUID) return api.db.DeleteIdentityImage(keyUID)