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  }