- 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
57 lines
1.7 KiB
Go
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
|
|
}
|