🖼 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
7 changed files with 168 additions and 16 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.87.1
|
||||
0.87.2
|
||||
|
|
BIN
_docs/image-center-crop-calculations.png
Normal file
BIN
_docs/image-center-crop-calculations.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9 MiB |
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue