// 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 }