feat avatar compress uploads to 20kb
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
@@ -9,11 +10,19 @@ import (
|
||||
"fmt"
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||
"image"
|
||||
"image/color"
|
||||
stddraw "image/draw"
|
||||
_ "image/gif"
|
||||
"image/jpeg"
|
||||
_ "image/png"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
xdraw "golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -31,6 +40,7 @@ var (
|
||||
const (
|
||||
maxNotifyEmails = 3 // Maximum number of notification emails per user
|
||||
maxInlineAvatarBytes = 100 * 1024
|
||||
targetAvatarBytes = 20 * 1024
|
||||
|
||||
// User-level rate limiting for notify email verification codes
|
||||
notifyCodeUserRateLimit = 5
|
||||
@@ -39,6 +49,11 @@ const (
|
||||
defaultUserIdentityRedirect = "/settings/profile"
|
||||
)
|
||||
|
||||
var (
|
||||
avatarScaleSteps = []float64{1, 0.92, 0.84, 0.76, 0.68, 0.6, 0.52, 0.44, 0.36}
|
||||
avatarQualitySteps = []int{88, 80, 72, 64, 56, 48, 40, 32}
|
||||
)
|
||||
|
||||
// UserListFilters contains all filter options for listing users
|
||||
type UserListFilters struct {
|
||||
Status string // User status filter
|
||||
@@ -432,6 +447,14 @@ func normalizeInlineUserAvatarInput(raw string) (UpsertUserAvatarInput, error) {
|
||||
return UpsertUserAvatarInput{}, ErrAvatarTooLarge
|
||||
}
|
||||
|
||||
if len(decoded) > targetAvatarBytes {
|
||||
decoded, contentType, err = compressInlineAvatar(decoded)
|
||||
if err != nil {
|
||||
return UpsertUserAvatarInput{}, err
|
||||
}
|
||||
raw = "data:" + contentType + ";base64," + base64.StdEncoding.EncodeToString(decoded)
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(decoded)
|
||||
return UpsertUserAvatarInput{
|
||||
StorageProvider: "inline",
|
||||
@@ -442,6 +465,38 @@ func normalizeInlineUserAvatarInput(raw string) (UpsertUserAvatarInput, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func compressInlineAvatar(decoded []byte) ([]byte, string, error) {
|
||||
src, _, err := image.Decode(bytes.NewReader(decoded))
|
||||
if err != nil {
|
||||
return nil, "", ErrAvatarInvalid
|
||||
}
|
||||
|
||||
srcBounds := src.Bounds()
|
||||
if srcBounds.Empty() {
|
||||
return nil, "", ErrAvatarInvalid
|
||||
}
|
||||
|
||||
for _, scale := range avatarScaleSteps {
|
||||
width := max(1, int(float64(srcBounds.Dx())*scale))
|
||||
height := max(1, int(float64(srcBounds.Dy())*scale))
|
||||
dst := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
stddraw.Draw(dst, dst.Bounds(), &image.Uniform{C: color.White}, image.Point{}, stddraw.Src)
|
||||
xdraw.CatmullRom.Scale(dst, dst.Bounds(), src, srcBounds, stddraw.Over, nil)
|
||||
|
||||
for _, quality := range avatarQualitySteps {
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, dst, &jpeg.Options{Quality: quality}); err != nil {
|
||||
return nil, "", ErrAvatarInvalid
|
||||
}
|
||||
if buf.Len() <= targetAvatarBytes {
|
||||
return buf.Bytes(), "image/jpeg", nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, "", ErrAvatarTooLarge
|
||||
}
|
||||
|
||||
func (s *UserService) buildEmailIdentitySummary(user *User) UserIdentitySummary {
|
||||
summary := UserIdentitySummary{
|
||||
Provider: "email",
|
||||
|
||||
Reference in New Issue
Block a user