This commit is contained in:
Tomas Dvorak
2025-10-19 17:16:57 +02:00
parent e9a63073e5
commit 77213f4e83
76 changed files with 9728 additions and 935 deletions
@@ -0,0 +1,389 @@
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,
})
}