first commit
This commit is contained in:
137
pkg/utils/image_processor.go
Normal file
137
pkg/utils/image_processor.go
Normal file
@@ -0,0 +1,137 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user