diff --git a/VERSION b/VERSION index 7921aa127..52ece7771 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.87.1 +0.87.2 diff --git a/_docs/image-center-crop-calculations.png b/_docs/image-center-crop-calculations.png new file mode 100644 index 000000000..67bfee983 Binary files /dev/null and b/_docs/image-center-crop-calculations.png differ diff --git a/images/decode.go b/images/decode.go index eee592848..140adf16d 100644 --- a/images/decode.go +++ b/images/decode.go @@ -1,15 +1,21 @@ package images import ( + "bytes" "errors" "image" "image/gif" "image/jpeg" "image/png" "io" + "io/ioutil" + "net/http" "os" + "time" "golang.org/x/image/webp" + + "github.com/ethereum/go-ethereum/log" ) func Decode(fileName string) (image.Image, error) { @@ -27,6 +33,29 @@ func Decode(fileName string) (image.Image, error) { 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) { // Read the first 14 bytes, used for performing image type checks before parsing the image data fb := make([]byte, 14) diff --git a/images/decode_test.go b/images/decode_test.go index 13a9e263e..d7e848aa3 100644 --- a/images/decode_test.go +++ b/images/decode_test.go @@ -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) { cs := []struct { Buf []byte diff --git a/images/main.go b/images/main.go index b31301282..64ac0e1a0 100644 --- a/images/main.go +++ b/images/main.go @@ -5,22 +5,10 @@ import ( "image" ) -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 - } - +func GenerateImageVariants(cImg image.Image) ([]*IdentityImage, error) { var iis []*IdentityImage + var err error + for _, s := range ResizeDimensions { rImg := Resize(s, cImg) @@ -44,3 +32,35 @@ func GenerateIdentityImages(filepath string, aX, aY, bX, bY int) ([]*IdentityIma 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) +} diff --git a/images/manipulation.go b/images/manipulation.go index 6e2bbd3ba..01a8bd0fd 100644 --- a/images/manipulation.go +++ b/images/manipulation.go @@ -43,3 +43,31 @@ func Crop(img image.Image, rect image.Rectangle) (image.Image, error) { 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) +} diff --git a/services/accounts/multiaccounts.go b/services/accounts/multiaccounts.go index 6b1be63ee..bb88f44ba 100644 --- a/services/accounts/multiaccounts.go +++ b/services/accounts/multiaccounts.go @@ -59,6 +59,20 @@ func (api *MultiAccountsAPI) StoreIdentityImage(keyUID, filepath string, aX, aY, 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 func (api *MultiAccountsAPI) DeleteIdentityImage(keyUID string) error { return api.db.DeleteIdentityImage(keyUID)