code.gitea.io/gitea@v1.22.3/modules/avatar/avatar.go (about) 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package avatar 5 6 import ( 7 "bytes" 8 "errors" 9 "fmt" 10 "image" 11 "image/color" 12 "image/png" 13 14 _ "image/gif" // for processing gif images 15 _ "image/jpeg" // for processing jpeg images 16 17 "code.gitea.io/gitea/modules/avatar/identicon" 18 "code.gitea.io/gitea/modules/setting" 19 20 "golang.org/x/image/draw" 21 22 _ "golang.org/x/image/webp" // for processing webp images 23 ) 24 25 // DefaultAvatarSize is the target CSS pixel size for avatar generation. It is 26 // multiplied by setting.Avatar.RenderedSizeFactor and the resulting size is the 27 // usual size of avatar image saved on server, unless the original file is smaller 28 // than the size after resizing. 29 const DefaultAvatarSize = 256 30 31 // RandomImageSize generates and returns a random avatar image unique to input data 32 // in custom size (height and width). 33 func RandomImageSize(size int, data []byte) (image.Image, error) { 34 // we use white as background, and use dark colors to draw blocks 35 imgMaker, err := identicon.New(size, color.White, identicon.DarkColors...) 36 if err != nil { 37 return nil, fmt.Errorf("identicon.New: %w", err) 38 } 39 return imgMaker.Make(data), nil 40 } 41 42 // RandomImage generates and returns a random avatar image unique to input data 43 // in default size (height and width). 44 func RandomImage(data []byte) (image.Image, error) { 45 return RandomImageSize(DefaultAvatarSize*setting.Avatar.RenderedSizeFactor, data) 46 } 47 48 // processAvatarImage process the avatar image data, crop and resize it if necessary. 49 // the returned data could be the original image if no processing is needed. 50 func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) { 51 imgCfg, imgType, err := image.DecodeConfig(bytes.NewReader(data)) 52 if err != nil { 53 return nil, fmt.Errorf("image.DecodeConfig: %w", err) 54 } 55 56 // for safety, only accept known types explicitly 57 if imgType != "png" && imgType != "jpeg" && imgType != "gif" && imgType != "webp" { 58 return nil, errors.New("unsupported avatar image type") 59 } 60 61 // do not process image which is too large, it would consume too much memory 62 if imgCfg.Width > setting.Avatar.MaxWidth { 63 return nil, fmt.Errorf("image width is too large: %d > %d", imgCfg.Width, setting.Avatar.MaxWidth) 64 } 65 if imgCfg.Height > setting.Avatar.MaxHeight { 66 return nil, fmt.Errorf("image height is too large: %d > %d", imgCfg.Height, setting.Avatar.MaxHeight) 67 } 68 69 // If the origin is small enough, just use it, then APNG could be supported, 70 // otherwise, if the image is processed later, APNG loses animation. 71 // And one more thing, webp is not fully supported, for animated webp, image.DecodeConfig works but Decode fails. 72 // So for animated webp, if the uploaded file is smaller than maxOriginSize, it will be used, if it's larger, there will be an error. 73 if len(data) < int(maxOriginSize) { 74 return data, nil 75 } 76 77 img, _, err := image.Decode(bytes.NewReader(data)) 78 if err != nil { 79 return nil, fmt.Errorf("image.Decode: %w", err) 80 } 81 82 // try to crop and resize the origin image if necessary 83 img = cropSquare(img) 84 85 targetSize := DefaultAvatarSize * setting.Avatar.RenderedSizeFactor 86 img = scale(img, targetSize, targetSize, draw.BiLinear) 87 88 // try to encode the cropped/resized image to png 89 bs := bytes.Buffer{} 90 if err = png.Encode(&bs, img); err != nil { 91 return nil, err 92 } 93 resized := bs.Bytes() 94 95 // usually the png compression is not good enough, use the original image (no cropping/resizing) if the origin is smaller 96 if len(data) <= len(resized) { 97 return data, nil 98 } 99 100 return resized, nil 101 } 102 103 // ProcessAvatarImage process the avatar image data, crop and resize it if necessary. 104 // the returned data could be the original image if no processing is needed. 105 func ProcessAvatarImage(data []byte) ([]byte, error) { 106 return processAvatarImage(data, setting.Avatar.MaxOriginSize) 107 } 108 109 // scale resizes the image to width x height using the given scaler. 110 func scale(src image.Image, width, height int, scale draw.Scaler) image.Image { 111 rect := image.Rect(0, 0, width, height) 112 dst := image.NewRGBA(rect) 113 scale.Scale(dst, rect, src, src.Bounds(), draw.Over, nil) 114 return dst 115 } 116 117 // cropSquare crops the largest square image from the center of the image. 118 // If the image is already square, it is returned unchanged. 119 func cropSquare(src image.Image) image.Image { 120 bounds := src.Bounds() 121 if bounds.Dx() == bounds.Dy() { 122 return src 123 } 124 125 var rect image.Rectangle 126 if bounds.Dx() > bounds.Dy() { 127 // width > height 128 size := bounds.Dy() 129 rect = image.Rect((bounds.Dx()-size)/2, 0, (bounds.Dx()+size)/2, size) 130 } else { 131 // width < height 132 size := bounds.Dx() 133 rect = image.Rect(0, (bounds.Dy()-size)/2, size, (bounds.Dy()+size)/2) 134 } 135 136 dst := image.NewRGBA(rect) 137 draw.Draw(dst, rect, src, rect.Min, draw.Src) 138 return dst 139 }