package controllers import ( "encoding/json" "fmt" "image" "image/jpeg" "image/png" "net/http" "os" "path/filepath" "strings" "time" "github.com/disintegration/imaging" "github.com/gin-gonic/gin" ) type ImageProcessingController struct{} // ImageProcessRequest represents the request body for image processing type ImageProcessRequest struct { ImageURL string `json:"image_url"` // URL of image to process Operation string `json:"operation"` // crop, resize, rotate, flip, filter Width int `json:"width"` // Target width (for resize) Height int `json:"height"` // Target height (for resize) CropX int `json:"crop_x"` // Crop coordinates CropY int `json:"crop_y"` CropWidth int `json:"crop_width"` CropHeight int `json:"crop_height"` Rotation int `json:"rotation"` // Rotation angle (90, 180, 270) FlipH bool `json:"flip_h"` // Flip horizontal FlipV bool `json:"flip_v"` // Flip vertical Brightness float64 `json:"brightness"` // -100 to 100 Contrast float64 `json:"contrast"` // -100 to 100 Saturation float64 `json:"saturation"` // -100 to 100 Blur float64 `json:"blur"` // 0 to 10 Sharpen float64 `json:"sharpen"` // 0 to 10 Grayscale bool `json:"grayscale"` // Convert to grayscale Quality int `json:"quality"` // JPEG quality 1-100 } // ProcessImage handles image processing operations func (ctrl *ImageProcessingController) ProcessImage(c *gin.Context) { var req ImageProcessRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) return } // Validate quality if req.Quality <= 0 || req.Quality > 100 { req.Quality = 85 // Default quality } // Load image img, format, err := ctrl.loadImage(req.ImageURL) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to load image: " + err.Error()}) return } // Apply operations based on request processedImg := img // Crop if req.CropWidth > 0 && req.CropHeight > 0 { processedImg = imaging.Crop(processedImg, image.Rect( req.CropX, req.CropY, req.CropX+req.CropWidth, req.CropY+req.CropHeight, )) } // Resize if req.Width > 0 || req.Height > 0 { width := req.Width height := req.Height if width == 0 { width = 0 // Auto width } if height == 0 { height = 0 // Auto height } processedImg = imaging.Resize(processedImg, width, height, imaging.Lanczos) } // Rotate if req.Rotation != 0 { switch req.Rotation % 360 { case 90, -270: processedImg = imaging.Rotate90(processedImg) case 180, -180: processedImg = imaging.Rotate180(processedImg) case 270, -90: processedImg = imaging.Rotate270(processedImg) } } // Flip if req.FlipH { processedImg = imaging.FlipH(processedImg) } if req.FlipV { processedImg = imaging.FlipV(processedImg) } // Brightness if req.Brightness != 0 { processedImg = imaging.AdjustBrightness(processedImg, req.Brightness) } // Contrast if req.Contrast != 0 { processedImg = imaging.AdjustContrast(processedImg, req.Contrast) } // Saturation if req.Saturation != 0 { processedImg = imaging.AdjustSaturation(processedImg, req.Saturation) } // Blur if req.Blur > 0 { processedImg = imaging.Blur(processedImg, req.Blur) } // Sharpen if req.Sharpen > 0 { processedImg = imaging.Sharpen(processedImg, req.Sharpen) } // Grayscale if req.Grayscale { processedImg = imaging.Grayscale(processedImg) } // Save processed image outputPath, err := ctrl.saveProcessedImage(processedImg, format, req.Quality) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + err.Error()}) return } // Return the new URL c.JSON(http.StatusOK, gin.H{ "url": outputPath, "format": format, }) } // loadImage loads an image from a URL or local path func (ctrl *ImageProcessingController) loadImage(imageURL string) (image.Image, string, error) { // Check if it's a local file path if strings.HasPrefix(imageURL, "/uploads/") || strings.HasPrefix(imageURL, "uploads/") { // Local file localPath := filepath.Join(".", imageURL) file, err := os.Open(localPath) if err != nil { return nil, "", fmt.Errorf("failed to open local file: %w", err) } defer file.Close() img, format, err := image.Decode(file) if err != nil { return nil, "", fmt.Errorf("failed to decode image: %w", err) } return img, format, nil } // HTTP URL resp, err := http.Get(imageURL) if err != nil { return nil, "", fmt.Errorf("failed to fetch image: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, "", fmt.Errorf("failed to fetch image: status %d", resp.StatusCode) } img, format, err := image.Decode(resp.Body) if err != nil { return nil, "", fmt.Errorf("failed to decode image: %w", err) } return img, format, nil } // saveProcessedImage saves the processed image and returns the path func (ctrl *ImageProcessingController) saveProcessedImage(img image.Image, format string, quality int) (string, error) { // Create uploads directory if it doesn't exist uploadsDir := "./uploads" if err := os.MkdirAll(uploadsDir, 0755); err != nil { return "", fmt.Errorf("failed to create uploads directory: %w", err) } // Generate unique filename timestamp := time.Now().UnixNano() / int64(time.Millisecond) filename := fmt.Sprintf("processed_%d.jpg", timestamp) outputPath := filepath.Join(uploadsDir, filename) // Create output file outFile, err := os.Create(outputPath) if err != nil { return "", fmt.Errorf("failed to create output file: %w", err) } defer outFile.Close() // Encode and save if format == "png" { err = png.Encode(outFile, img) } else { err = jpeg.Encode(outFile, img, &jpeg.Options{Quality: quality}) } if err != nil { return "", fmt.Errorf("failed to encode image: %w", err) } // Return relative URL return "/uploads/" + filename, nil } // CropAndUpload handles image cropping with upload func (ctrl *ImageProcessingController) CropAndUpload(c *gin.Context) { // Get image from form data file, err := c.FormFile("image") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "No image file provided"}) return } // Get crop parameters var cropParams struct { X int `json:"x"` Y int `json:"y"` Width int `json:"width"` Height int `json:"height"` } cropData := c.PostForm("crop_data") if cropData != "" { if err := json.Unmarshal([]byte(cropData), &cropParams); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid crop parameters"}) return } } quality := 85 if q := c.PostForm("quality"); q != "" { fmt.Sscanf(q, "%d", &quality) } maxWidth := 1500 if mw := c.PostForm("max_width"); mw != "" { fmt.Sscanf(mw, "%d", &maxWidth) } // Open uploaded file src, err := file.Open() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open uploaded file"}) return } defer src.Close() // Decode image img, format, err := image.Decode(src) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid image format"}) return } // Apply crop if parameters provided processedImg := img if cropParams.Width > 0 && cropParams.Height > 0 { processedImg = imaging.Crop(processedImg, image.Rect( cropParams.X, cropParams.Y, cropParams.X+cropParams.Width, cropParams.Y+cropParams.Height, )) } // Resize if larger than max width bounds := processedImg.Bounds() if bounds.Dx() > maxWidth { processedImg = imaging.Resize(processedImg, maxWidth, 0, imaging.Lanczos) } // Save to uploads outputPath, err := ctrl.saveProcessedImage(processedImg, format, quality) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save processed image"}) return } c.JSON(http.StatusOK, gin.H{ "url": outputPath, }) } // QuickEdit handles common quick edits in one call func (ctrl *ImageProcessingController) QuickEdit(c *gin.Context) { var req struct { ImageURL string `json:"image_url"` Width int `json:"width"` Rotation int `json:"rotation"` FlipH bool `json:"flip_h"` FlipV bool `json:"flip_v"` Brightness float64 `json:"brightness"` Contrast float64 `json:"contrast"` Saturation float64 `json:"saturation"` Grayscale bool `json:"grayscale"` Quality int `json:"quality"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) return } if req.Quality <= 0 || req.Quality > 100 { req.Quality = 85 } // Load image img, format, err := ctrl.loadImage(req.ImageURL) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to load image: " + err.Error()}) return } processedImg := img // Resize if width specified if req.Width > 0 { processedImg = imaging.Resize(processedImg, req.Width, 0, imaging.Lanczos) } // Rotate if req.Rotation != 0 { switch req.Rotation % 360 { case 90, -270: processedImg = imaging.Rotate90(processedImg) case 180, -180: processedImg = imaging.Rotate180(processedImg) case 270, -90: processedImg = imaging.Rotate270(processedImg) } } // Flip if req.FlipH { processedImg = imaging.FlipH(processedImg) } if req.FlipV { processedImg = imaging.FlipV(processedImg) } // Brightness/Contrast/Saturation if req.Brightness != 0 { processedImg = imaging.AdjustBrightness(processedImg, req.Brightness) } if req.Contrast != 0 { processedImg = imaging.AdjustContrast(processedImg, req.Contrast) } if req.Saturation != 0 { processedImg = imaging.AdjustSaturation(processedImg, req.Saturation) } // Grayscale if req.Grayscale { processedImg = imaging.Grayscale(processedImg) } // Save outputPath, err := ctrl.saveProcessedImage(processedImg, format, req.Quality) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image"}) return } c.JSON(http.StatusOK, gin.H{ "url": outputPath, }) }