feat: add avatar cropping with Cropper.js and server-side resize
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -13,6 +16,9 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
xdraw "golang.org/x/image/draw"
|
||||
_ "golang.org/x/image/webp"
|
||||
|
||||
"go_blog/models"
|
||||
)
|
||||
|
||||
@@ -147,3 +153,92 @@ func UpdateProfile(db *gorm.DB, storagePath string) gin.HandlerFunc {
|
||||
c.Redirect(http.StatusFound, "/profile?saved=1")
|
||||
}
|
||||
}
|
||||
|
||||
// UploadAvatar handles AJAX avatar upload with cropping.
|
||||
func UploadAvatar(db *gorm.DB, storagePath string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
userID := session.Get("user_id")
|
||||
|
||||
var user models.User
|
||||
if err := db.First(&user, userID).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := c.Request.FormFile("avatar")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Validate extension.
|
||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||
if ext == "" || (ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".gif" && ext != ".webp") {
|
||||
ext = ".jpg"
|
||||
}
|
||||
|
||||
// Read file bytes for image processing.
|
||||
imgBytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read file"})
|
||||
return
|
||||
}
|
||||
|
||||
// Decode, resize, and re-encode the image.
|
||||
processedBytes, finalExt, err := processAvatar(imgBytes, ext)
|
||||
if err != nil {
|
||||
// Fall back to saving raw bytes if processing fails.
|
||||
processedBytes = imgBytes
|
||||
finalExt = ext
|
||||
}
|
||||
|
||||
// Ensure avatar directory exists.
|
||||
avatarDir := filepath.Join(storagePath, "avatars")
|
||||
os.MkdirAll(avatarDir, 0755)
|
||||
|
||||
// Remove old avatar file.
|
||||
if user.Avatar != "" {
|
||||
oldPath := filepath.Join(avatarDir, user.Avatar)
|
||||
os.Remove(oldPath)
|
||||
}
|
||||
|
||||
// Save processed avatar.
|
||||
savedName := fmt.Sprintf("%d%s", user.ID, finalExt)
|
||||
savedPath := filepath.Join(avatarDir, savedName)
|
||||
if err := os.WriteFile(savedPath, processedBytes, 0644); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save avatar"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update user record.
|
||||
user.Avatar = savedName
|
||||
db.Save(&user)
|
||||
|
||||
// Update session.
|
||||
session.Set("avatar", savedName)
|
||||
session.Save()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"avatar": savedName})
|
||||
}
|
||||
}
|
||||
|
||||
// processAvatar decodes, resizes to 256x256, and re-encodes an avatar image as JPEG.
|
||||
func processAvatar(imgBytes []byte, ext string) ([]byte, string, error) {
|
||||
src, _, err := image.Decode(bytes.NewReader(imgBytes))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
const targetSize = 256
|
||||
dst := image.NewRGBA(image.Rect(0, 0, targetSize, targetSize))
|
||||
xdraw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), xdraw.Src, nil)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, dst, &jpeg.Options{Quality: 85}); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return buf.Bytes(), ".jpg", nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user