更新
This commit is contained in:
+83
@@ -0,0 +1,83 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
const (
|
||||
avatarDisplaySize = 640
|
||||
avatarThumbnailSize = 256
|
||||
maxAvatarPixels = 24_000_000
|
||||
)
|
||||
|
||||
// Bound peak decoding memory on small servers. Do not queue decoded photos in
|
||||
// memory when multiple clients upload avatars at the same time.
|
||||
var avatarProcessing = make(chan struct{}, 1)
|
||||
var errAvatarBusy = errors.New("头像正在处理中,请稍后重试")
|
||||
|
||||
type avatarImages struct {
|
||||
Display, Thumbnail []byte
|
||||
Width, Height int
|
||||
}
|
||||
|
||||
func createAvatarImages(ctx context.Context, source io.ReadSeeker) (avatarImages, error) {
|
||||
select {
|
||||
case avatarProcessing <- struct{}{}:
|
||||
defer func() { <-avatarProcessing }()
|
||||
default:
|
||||
return avatarImages{}, errAvatarBusy
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return avatarImages{}, err
|
||||
}
|
||||
config, format, err := image.DecodeConfig(source)
|
||||
if err != nil || (format != "jpeg" && format != "png" && format != "gif" && format != "webp") {
|
||||
return avatarImages{}, errors.New("无法读取头像,请选择有效的 JPG、PNG、GIF 或 WebP 图片")
|
||||
}
|
||||
if config.Width <= 0 || config.Height <= 0 || config.Width > 16384 || config.Height > 16384 || int64(config.Width)*int64(config.Height) > maxAvatarPixels {
|
||||
return avatarImages{}, errors.New("头像分辨率过大,请选择不超过 2400 万像素的图片")
|
||||
}
|
||||
if _, err = source.Seek(0, io.SeekStart); err != nil {
|
||||
return avatarImages{}, err
|
||||
}
|
||||
// Orient phone photos before resizing. Re-encoding also removes EXIF/GPS
|
||||
// from the display variants. Animated images use their first decoded frame.
|
||||
src, err := imaging.Decode(source, imaging.AutoOrientation(true))
|
||||
if err != nil {
|
||||
return avatarImages{}, errors.New("头像文件损坏或格式暂不支持,请重新选择图片")
|
||||
}
|
||||
if err = ctx.Err(); err != nil {
|
||||
return avatarImages{}, err
|
||||
}
|
||||
display := imaging.Fit(src, avatarDisplaySize, avatarDisplaySize, imaging.Lanczos)
|
||||
thumbnail := imaging.Fit(display, avatarThumbnailSize, avatarThumbnailSize, imaging.Lanczos)
|
||||
result := avatarImages{Width: display.Bounds().Dx(), Height: display.Bounds().Dy()}
|
||||
if result.Display, err = avatarJPEG(display, 82); err != nil {
|
||||
return avatarImages{}, err
|
||||
}
|
||||
if result.Thumbnail, err = avatarJPEG(thumbnail, 78); err != nil {
|
||||
return avatarImages{}, err
|
||||
}
|
||||
_, err = source.Seek(0, io.SeekStart)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func avatarJPEG(src image.Image, quality int) ([]byte, error) {
|
||||
// Composite transparency onto white instead of producing black PNG edges.
|
||||
opaque := image.NewRGBA(src.Bounds())
|
||||
draw.Draw(opaque, opaque.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
|
||||
draw.Draw(opaque, opaque.Bounds(), src, src.Bounds().Min, draw.Over)
|
||||
var buffer bytes.Buffer
|
||||
err := jpeg.Encode(&buffer, opaque, &jpeg.Options{Quality: quality})
|
||||
return buffer.Bytes(), err
|
||||
}
|
||||
Reference in New Issue
Block a user