138 lines
3.9 KiB
Go
138 lines
3.9 KiB
Go
package utils
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"mime/multipart"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"gobeyhan/config"
|
|
|
|
"github.com/chai2010/webp"
|
|
"github.com/disintegration/imaging"
|
|
)
|
|
|
|
type ImageOptions struct {
|
|
Width int
|
|
Height int
|
|
Quality float32 // 1-100 (WebP uses float32)
|
|
Format string // "webp", "jpg", "png"
|
|
Mode string // "cover", "contain", "resize"
|
|
}
|
|
|
|
// SaveOptimizedImage processes and saves an image
|
|
// Returns the relative path to the saved file (e.g., "/uploads/avatars/filename.webp")
|
|
func SaveOptimizedImage(fileHeader *multipart.FileHeader, uploadDir string, userID string, opts *ImageOptions) (string, error) {
|
|
// If opts is nil, use defaults from config
|
|
if opts == nil {
|
|
opts = &ImageOptions{
|
|
Width: config.AppConfig.AvatarWidth,
|
|
Height: config.AppConfig.AvatarHeight,
|
|
Quality: float32(config.AppConfig.AvatarQuality),
|
|
Format: config.AppConfig.AvatarFormat,
|
|
Mode: config.AppConfig.AvatarMode,
|
|
}
|
|
}
|
|
|
|
// Open the file
|
|
srcFile, err := fileHeader.Open()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to open file: %v", err)
|
|
}
|
|
defer srcFile.Close()
|
|
|
|
// Decode image
|
|
img, _, err := image.Decode(srcFile)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to decode image: %v", err)
|
|
}
|
|
|
|
// Resize logic
|
|
if opts.Width > 0 || opts.Height > 0 {
|
|
switch strings.ToLower(opts.Mode) {
|
|
case "cover":
|
|
// Fill requires both dimensions to be effective for cropping.
|
|
// If one is missing, we fall back to Resize which preserves aspect ratio.
|
|
if opts.Width > 0 && opts.Height > 0 {
|
|
img = imaging.Fill(img, opts.Width, opts.Height, imaging.Center, imaging.Lanczos)
|
|
} else {
|
|
img = imaging.Resize(img, opts.Width, opts.Height, imaging.Lanczos)
|
|
}
|
|
case "contain":
|
|
// Fit fits the image within the box, preserving aspect ratio.
|
|
// If one dimension is 0, Fit might behave like Resize(w, h) if implementation allows,
|
|
// but imaging.Fit usually expects a box.
|
|
// If one is 0, we assume the user wants to limit the other dimension.
|
|
if opts.Width > 0 && opts.Height > 0 {
|
|
img = imaging.Fit(img, opts.Width, opts.Height, imaging.Lanczos)
|
|
} else {
|
|
img = imaging.Resize(img, opts.Width, opts.Height, imaging.Lanczos)
|
|
}
|
|
default:
|
|
// "resize" or empty: Resize preserves aspect ratio if one arg is 0.
|
|
// If both are provided, it stretches unless we use Fill/Fit.
|
|
// imaging.Resize stretches if both are non-zero.
|
|
img = imaging.Resize(img, opts.Width, opts.Height, imaging.Lanczos)
|
|
}
|
|
}
|
|
|
|
// Determine output format and filename
|
|
ext := "." + strings.ToLower(opts.Format)
|
|
if opts.Format == "" {
|
|
ext = ".webp" // Default to WebP
|
|
opts.Format = "webp"
|
|
}
|
|
|
|
// Generate filename
|
|
filename := fmt.Sprintf("%s_%d%s", userID, time.Now().UnixNano(), ext)
|
|
|
|
// Ensure directory exists
|
|
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
|
return "", fmt.Errorf("failed to create directory: %v", err)
|
|
}
|
|
|
|
fullPath := filepath.Join(uploadDir, filename)
|
|
outFile, err := os.Create(fullPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create output file: %v", err)
|
|
}
|
|
defer outFile.Close()
|
|
|
|
// Encode
|
|
switch strings.ToLower(opts.Format) {
|
|
case "jpg", "jpeg":
|
|
quality := int(opts.Quality)
|
|
if quality < 1 || quality > 100 {
|
|
quality = 90
|
|
}
|
|
err = jpeg.Encode(outFile, img, &jpeg.Options{Quality: quality})
|
|
case "png":
|
|
err = png.Encode(outFile, img) // PNG is lossless
|
|
case "webp", "":
|
|
err = webp.Encode(outFile, img, &webp.Options{Lossless: false, Quality: opts.Quality})
|
|
default:
|
|
// Fallback to WebP
|
|
err = webp.Encode(outFile, img, &webp.Options{Lossless: false, Quality: opts.Quality})
|
|
}
|
|
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to encode image: %v", err)
|
|
}
|
|
|
|
// Return relative path
|
|
relPath := filepath.Join(uploadDir, filename)
|
|
if strings.HasPrefix(relPath, ".") {
|
|
relPath = relPath[1:]
|
|
}
|
|
if !strings.HasPrefix(relPath, "/") {
|
|
relPath = "/" + relPath
|
|
}
|
|
|
|
return relPath, nil
|
|
}
|