2014-03-23 14:13:23 +04:00
// Copyright 2014 The Gogs Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2014-03-23 14:13:23 +04:00
2014-03-23 08:24:09 +04:00
package avatar
import (
2019-05-25 14:46:14 +03:00
"bytes"
2023-05-13 21:59:11 +03:00
"errors"
2014-03-23 08:24:09 +04:00
"fmt"
2014-03-23 11:55:27 +04:00
"image"
2021-11-19 20:10:41 +03:00
"image/color"
2023-05-13 21:59:11 +03:00
"image/png"
2020-10-14 16:07:51 +03:00
2021-06-05 15:32:19 +03:00
_ "image/gif" // for processing gif images
_ "image/jpeg" // for processing jpeg images
2014-03-23 11:55:27 +04:00
2021-11-19 20:10:41 +03:00
"code.gitea.io/gitea/modules/avatar/identicon"
2019-05-25 14:46:14 +03:00
"code.gitea.io/gitea/modules/setting"
2023-07-20 14:52:42 +03:00
"golang.org/x/image/draw"
2023-04-21 20:15:49 +03:00
_ "golang.org/x/image/webp" // for processing webp images
2014-03-23 08:24:09 +04:00
)
2023-05-13 21:59:11 +03:00
// DefaultAvatarSize is the target CSS pixel size for avatar generation. It is
// multiplied by setting.Avatar.RenderedSizeFactor and the resulting size is the
// usual size of avatar image saved on server, unless the original file is smaller
// than the size after resizing.
const DefaultAvatarSize = 256
2015-08-09 06:46:10 +03:00
2016-11-25 11:37:04 +03:00
// RandomImageSize generates and returns a random avatar image unique to input data
2016-02-21 01:10:05 +03:00
// in custom size (height and width).
2016-02-15 07:14:55 +03:00
func RandomImageSize ( size int , data [ ] byte ) ( image . Image , error ) {
2021-11-19 20:10:41 +03:00
// we use white as background, and use dark colors to draw blocks
imgMaker , err := identicon . New ( size , color . White , identicon . DarkColors ... )
2015-08-09 06:46:10 +03:00
if err != nil {
2022-10-24 22:29:17 +03:00
return nil , fmt . Errorf ( "identicon.New: %w" , err )
2015-08-09 06:46:10 +03:00
}
return imgMaker . Make ( data ) , nil
}
2016-02-21 01:10:05 +03:00
// RandomImage generates and returns a random avatar image unique to input data
// in default size (height and width).
2016-02-15 07:14:55 +03:00
func RandomImage ( data [ ] byte ) ( image . Image , error ) {
2023-05-13 21:59:11 +03:00
return RandomImageSize ( DefaultAvatarSize * setting . Avatar . RenderedSizeFactor , data )
2014-03-23 08:24:09 +04:00
}
2019-05-25 14:46:14 +03:00
2023-05-13 21:59:11 +03:00
// processAvatarImage process the avatar image data, crop and resize it if necessary.
// the returned data could be the original image if no processing is needed.
func processAvatarImage ( data [ ] byte , maxOriginSize int64 ) ( [ ] byte , error ) {
imgCfg , imgType , err := image . DecodeConfig ( bytes . NewReader ( data ) )
2019-05-25 14:46:14 +03:00
if err != nil {
2023-05-13 21:59:11 +03:00
return nil , fmt . Errorf ( "image.DecodeConfig: %w" , err )
2019-05-25 14:46:14 +03:00
}
2023-05-13 21:59:11 +03:00
// for safety, only accept known types explicitly
if imgType != "png" && imgType != "jpeg" && imgType != "gif" && imgType != "webp" {
return nil , errors . New ( "unsupported avatar image type" )
}
// do not process image which is too large, it would consume too much memory
2020-10-14 16:07:51 +03:00
if imgCfg . Width > setting . Avatar . MaxWidth {
2023-05-13 21:59:11 +03:00
return nil , fmt . Errorf ( "image width is too large: %d > %d" , imgCfg . Width , setting . Avatar . MaxWidth )
2019-05-25 14:46:14 +03:00
}
2020-10-14 16:07:51 +03:00
if imgCfg . Height > setting . Avatar . MaxHeight {
2023-05-13 21:59:11 +03:00
return nil , fmt . Errorf ( "image height is too large: %d > %d" , imgCfg . Height , setting . Avatar . MaxHeight )
}
// If the origin is small enough, just use it, then APNG could be supported,
// otherwise, if the image is processed later, APNG loses animation.
// And one more thing, webp is not fully supported, for animated webp, image.DecodeConfig works but Decode fails.
// 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.
if len ( data ) < int ( maxOriginSize ) {
return data , nil
2019-05-25 14:46:14 +03:00
}
img , _ , err := image . Decode ( bytes . NewReader ( data ) )
if err != nil {
2023-05-13 21:59:11 +03:00
return nil , fmt . Errorf ( "image.Decode: %w" , err )
2019-05-25 14:46:14 +03:00
}
2023-05-13 21:59:11 +03:00
// try to crop and resize the origin image if necessary
2023-07-20 14:52:42 +03:00
img = cropSquare ( img )
2019-05-25 14:46:14 +03:00
2023-07-20 14:52:42 +03:00
targetSize := DefaultAvatarSize * setting . Avatar . RenderedSizeFactor
img = scale ( img , targetSize , targetSize , draw . BiLinear )
2023-05-13 21:59:11 +03:00
// try to encode the cropped/resized image to png
bs := bytes . Buffer { }
if err = png . Encode ( & bs , img ) ; err != nil {
return nil , err
}
resized := bs . Bytes ( )
// usually the png compression is not good enough, use the original image (no cropping/resizing) if the origin is smaller
if len ( data ) <= len ( resized ) {
return data , nil
}
return resized , nil
}
// ProcessAvatarImage process the avatar image data, crop and resize it if necessary.
// the returned data could be the original image if no processing is needed.
func ProcessAvatarImage ( data [ ] byte ) ( [ ] byte , error ) {
return processAvatarImage ( data , setting . Avatar . MaxOriginSize )
2019-05-25 14:46:14 +03:00
}
2023-07-20 14:52:42 +03:00
// scale resizes the image to width x height using the given scaler.
func scale ( src image . Image , width , height int , scale draw . Scaler ) image . Image {
rect := image . Rect ( 0 , 0 , width , height )
dst := image . NewRGBA ( rect )
scale . Scale ( dst , rect , src , src . Bounds ( ) , draw . Over , nil )
return dst
}
// cropSquare crops the largest square image from the center of the image.
// If the image is already square, it is returned unchanged.
func cropSquare ( src image . Image ) image . Image {
bounds := src . Bounds ( )
if bounds . Dx ( ) == bounds . Dy ( ) {
return src
}
var rect image . Rectangle
if bounds . Dx ( ) > bounds . Dy ( ) {
// width > height
size := bounds . Dy ( )
rect = image . Rect ( ( bounds . Dx ( ) - size ) / 2 , 0 , ( bounds . Dx ( ) + size ) / 2 , size )
} else {
// width < height
size := bounds . Dx ( )
rect = image . Rect ( 0 , ( bounds . Dy ( ) - size ) / 2 , size , ( bounds . Dy ( ) + size ) / 2 )
}
dst := image . NewRGBA ( rect )
draw . Draw ( dst , rect , src , rect . Min , draw . Src )
return dst
}