Files
ds-galo/backend/internal/avatar/avatar.go
T
iu 04617d1cdb feat(profile): add round avatar with upload and cropping
- backend: user_avatar_objects binding (PG+SQLite), GC preserves avatar blobs,
  avatar service (EXIF-stripping WebP 128/512), public avatar read route
- web: avatar upload/crop on profile, round avatar in header/members/comments/author
- android: avatar upload/crop, round avatar on profile and albums header
- openspec: add-user-avatar proposal/design/specs/tasks
2026-10-12 16:16:47 +03:00

57 lines
1.7 KiB
Go

// Package avatar manages a user's profile photo: it normalises an uploaded
// image into square WebP variants and keeps them as journaled objects bound to
// the user, so the object ledger's garbage collection preserves them.
package avatar
import (
"bytes"
"errors"
"image"
"github.com/disintegration/imaging"
"github.com/gen2brain/webp"
)
// Canonical avatar sizes. The small variant serves lists and headers; the large
// one serves the profile page.
const (
SizeLarge = 512
SizeSmall = 128
)
// maxUploadBytes caps an avatar upload. Avatars are small, so a generous
// general image limit would let a client stream needlessly.
const maxUploadBytes = 10 << 20
var errInvalidImage = errors.New("invalid image")
// normalize decodes an uploaded image and produces two square WebP variants.
// Decoding honours the EXIF orientation; re-encoding discards all metadata
// (including GPS), so the stored avatar carries none of the original's.
func normalize(data []byte) (large, small []byte, err error) {
img, err := imaging.Decode(bytes.NewReader(data), imaging.AutoOrientation(true))
if err != nil {
return nil, nil, errInvalidImage
}
large, err = fillWebP(img, SizeLarge)
if err != nil {
return nil, nil, err
}
small, err = fillWebP(img, SizeSmall)
if err != nil {
return nil, nil, err
}
return large, small, nil
}
// fillWebP centre-crops the image to a square of the given size and encodes it
// as WebP.
func fillWebP(img image.Image, size int) ([]byte, error) {
square := imaging.Fill(img, size, size, imaging.Center, imaging.Lanczos)
var buf bytes.Buffer
if err := webp.Encode(&buf, square, webp.Options{Quality: 82}); err != nil {
return nil, err
}
return buf.Bytes(), nil
}