🖼 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:
parent
95dcbef5e5
commit
a6e7ff6ddd
Binary file not shown.
After Width: | Height: | Size: 9.0 MiB |
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue