github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/avatars/utils.go (about)

     1  package avatars
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/base64"
     7  	"image"
     8  	"image/color"
     9  	"image/png"
    10  	"io"
    11  	"net/url"
    12  	"os"
    13  	"runtime"
    14  
    15  	"github.com/keybase/client/go/chat/globals"
    16  	"github.com/keybase/client/go/libkb"
    17  	"github.com/keybase/client/go/protocol/keybase1"
    18  	"golang.org/x/image/draw"
    19  )
    20  
    21  var AllFormats = []keybase1.AvatarFormat{
    22  	"square_192",
    23  	"square_256",
    24  	"square_960",
    25  	"square_360",
    26  	"square_200",
    27  	"square_40",
    28  }
    29  
    30  const avatarPlaceholder = "iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAAAAAB3tzPbAAADwElEQVR4Ae3bB5ayWBDF8dn/mi6CqNgNnYzntYFGMAs8ljAnT57PUFQ9z6n/Dn4djHV/a548BZCkAAUoQAEKUIACFKAABShAAQpQgAIUoAAFKEABClCAAhSgAAUoQAEKOG2+R+/xcDAYxu+j783pqQB7k/j4W35i9s8BOEy7+I+604PrALuK8L9FK+swoF6E+GXhonYVkHZxVd3UScAxxtXFR/cAxsMNecYxQJXgxpLKJcA+xM2Fe3cAuY878nNXAKmHu/JSNwBr3N3aBcDGw915G3nAroMH6uykAecADxWchQEveLAXWcAUDzeVBBQgqJAD2AEIGlgxgAFJRgpw7oCkzlkIMAZRYxlA2QFRnVIEMANZMwlA7YMsvxYApCAsFQDEICzmB1xA2oUdsARpS3bAB0j7YAcEIC3gBhxA3IEZsAJxK2bADMTNmAFvIO6NGdAHcX1mgA/ifGYAyOMFVCCvYgVcQN6FFXACeSdWQAnySlaABXmWFdB4IM5reAEBiAuYAUMQN2QGfIG4L2aAAXGGGZCBuIwZUIG4ihnQRCAtargBE5A2YQfkIC1nB9gAhAWWHdBM6P+CeAF7ELYXADQR/esIXkAKsn5EAE2f/iMVXsAaRK1JAfz/BVEjBdiDpL0YoBmBoFEjByhDPFxYCgKanYcH83ayF1sGD2akj/7e8VDvjTSgfsUDvdbigKYc4O4GpQuXu5c+7qx/ceN2uhriroaVK9frdYI7Smp39gN2ipubWqcmKHmAmwpy1zY05wQ3lJwdnGFlPVxZL3NzR1bPA1xRMK+dnSJW5peEwFROj0Hr1Sv+p9dV7f4c92xePPxL3os5P8sgus5nSQ9/qpfM8vrZJun2UGTpcplmxcHqpl4BCng2QHna5lmarpbLVZpm+fZUPgfAnjbf4zjqevhHXjeKx9+bk3UVUBXmo4cr6n2YonIMUP6M+rip/uindAWQz+59TzzL5QHFKMADBaNCEnCcdPFw3clRBmDTGETFqWUHVKYLwrqmYgXUhv7kzNRsAGsCtFBgLA+g6KOl+gUD4PKJFvu8tA3IfLSan7UKsBO03sS2BzhHYCg6twU4hGApPLQD2AVgKti1Acg7YKuT0wMOPhjzD9SASwjWwgstwA7B3NCSAmZgb0YJ2Htgz9sTAiIIFNEBUoj0QwaIIFJEBSggVEEEGEGoEREghFAhDeAIsY4kgCXEWpIA5hBrTgIYQawRCeAdYr2TABKIlShAAf+WAhSgAAUoQAEKUIACFKAABShAAQpQgAIUoIDfAUJ3U+9hO4+uAAAAAElFTkSuQmCC"
    31  
    32  func getAvatarPlaceholder() io.ReadCloser {
    33  	dat, _ := base64.StdEncoding.DecodeString(avatarPlaceholder)
    34  	return io.NopCloser(bytes.NewBuffer(dat))
    35  }
    36  
    37  func FetchAvatar(ctx context.Context, g *globals.Context, username string) (res io.ReadCloser, err error) {
    38  	avMap, err := g.GetAvatarLoader().LoadUsers(libkb.NewMetaContext(ctx, g.ExternalG()), []string{username}, []keybase1.AvatarFormat{"square_192"})
    39  	if err != nil {
    40  		return res, err
    41  	}
    42  	avatarURL := avMap.Picmap[username]["square_192"].String()
    43  	if len(avatarURL) == 0 {
    44  		return getAvatarPlaceholder(), nil
    45  	}
    46  
    47  	var avatarReader io.ReadCloser
    48  	parsed, err := url.Parse(avatarURL)
    49  	if err != nil {
    50  		return res, err
    51  	}
    52  	switch parsed.Scheme {
    53  	case "http", "https":
    54  		resp, err := libkb.ProxyHTTPGet(g.ExternalG(), g.GetEnv(), avatarURL, "FetchAvatar")
    55  		if err != nil {
    56  			return res, err
    57  		}
    58  		if resp.StatusCode >= 400 {
    59  			avatarReader = getAvatarPlaceholder()
    60  		} else {
    61  			avatarReader = resp.Body
    62  		}
    63  	case "file":
    64  		filePath := parsed.Path
    65  		if runtime.GOOS == "windows" && len(filePath) > 0 {
    66  			filePath = filePath[1:]
    67  		}
    68  		avatarReader, err = os.Open(filePath)
    69  		if err != nil {
    70  			return res, err
    71  		}
    72  	}
    73  	return avatarReader, nil
    74  }
    75  
    76  func GetBorderedCircleAvatar(ctx context.Context, g *globals.Context, username string, avatarSize, outerBorder, innerBorder int) (res io.ReadCloser, length int64, err error) {
    77  	white := color.RGBA{255, 255, 255, 255}
    78  	blue := color.RGBA{76, 142, 255, 255}
    79  	avatarReader, err := FetchAvatar(ctx, g, username)
    80  	if err != nil {
    81  		return res, length, err
    82  	}
    83  	defer avatarReader.Close()
    84  	avatarImg, _, err := image.Decode(avatarReader)
    85  	if err != nil {
    86  		return res, length, err
    87  	}
    88  	scaledAvatar := image.NewRGBA(image.Rect(0, 0, avatarSize, avatarSize))
    89  	draw.BiLinear.Scale(scaledAvatar, scaledAvatar.Bounds(), avatarImg, avatarImg.Bounds(), draw.Over, nil)
    90  	avatarRadius := avatarSize / 2
    91  	borderedRadius := avatarRadius + outerBorder + innerBorder
    92  	resultSize := borderedRadius * 2
    93  
    94  	bounds := image.Rect(0, 0, resultSize, resultSize)
    95  	middle := image.Point{borderedRadius, borderedRadius}
    96  	iconRect := image.Rect(middle.X-avatarRadius, middle.Y-avatarRadius, middle.X+avatarRadius, middle.Y+avatarRadius)
    97  	mask := &circleMask{image.Point{avatarRadius, avatarRadius}, avatarRadius}
    98  
    99  	result := image.NewRGBA(bounds)
   100  
   101  	draw.Draw(result, bounds, &circle{middle, borderedRadius, blue}, image.Point{}, draw.Over)
   102  	draw.Draw(result, bounds, &circle{middle, avatarRadius + innerBorder, white}, image.Point{}, draw.Over)
   103  	draw.DrawMask(result, iconRect, scaledAvatar, image.Point{}, mask, image.Point{}, draw.Over)
   104  
   105  	var buf bytes.Buffer
   106  	err = png.Encode(&buf, result)
   107  	if err != nil {
   108  		return res, length, err
   109  	}
   110  	return io.NopCloser(bytes.NewReader(buf.Bytes())), int64(buf.Len()), nil
   111  }
   112  
   113  type circleMask struct {
   114  	p image.Point
   115  	r int
   116  }
   117  
   118  func (c *circleMask) ColorModel() color.Model {
   119  	return color.AlphaModel
   120  }
   121  
   122  func (c *circleMask) Bounds() image.Rectangle {
   123  	return image.Rect(c.p.X-c.r, c.p.Y-c.r, c.p.X+c.r, c.p.Y+c.r)
   124  }
   125  
   126  func (c *circleMask) At(x, y int) color.Color {
   127  	xx, yy, rr := float64(x-c.p.X)+1, float64(y-c.p.Y)+1, float64(c.r)
   128  	if xx*xx+yy*yy < rr*rr {
   129  		return color.Alpha{255}
   130  	}
   131  	return color.Alpha{0}
   132  }
   133  
   134  type circle struct {
   135  	p    image.Point
   136  	r    int
   137  	fill color.Color
   138  }
   139  
   140  func (c *circle) ColorModel() color.Model {
   141  	return color.RGBAModel
   142  }
   143  
   144  func (c *circle) Bounds() image.Rectangle {
   145  	return image.Rect(c.p.X-c.r, c.p.Y-c.r, c.p.X+c.r, c.p.Y+c.r)
   146  }
   147  
   148  func (c *circle) At(x, y int) color.Color {
   149  	xx, yy, rr := float64(x-c.p.X)+1, float64(y-c.p.Y)+1, float64(c.r)
   150  	if xx*xx+yy*yy < rr*rr {
   151  		return c.fill
   152  	}
   153  	return color.RGBA{0, 0, 0, 0}
   154  }