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 }