github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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 }