mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
461 lines
12 KiB
Go
461 lines
12 KiB
Go
package controllers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"image"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"fotbal-club/internal/config"
|
|
|
|
"github.com/disintegration/imaging"
|
|
"github.com/gin-gonic/gin"
|
|
_ "golang.org/x/image/webp"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
scheme := "http"
|
|
if c.Request.TLS != nil || strings.EqualFold(c.Request.Header.Get("X-Forwarded-Proto"), "https") {
|
|
scheme = "https"
|
|
}
|
|
host := c.Request.Host
|
|
if xf := strings.TrimSpace(c.Request.Header.Get("X-Forwarded-Host")); xf != "" {
|
|
xfl := strings.ToLower(xf)
|
|
if !strings.Contains(xfl, ":3000") && !strings.HasPrefix(xfl, "localhost:") && !strings.HasPrefix(xfl, "127.0.0.1:") {
|
|
host = xf
|
|
}
|
|
}
|
|
absolute := scheme + "://" + host + outputPath
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"url": outputPath,
|
|
"absolute_url": absolute,
|
|
"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 under configured uploads dir
|
|
base := config.AppConfig.UploadDir
|
|
if strings.TrimSpace(base) == "" {
|
|
base = "./uploads"
|
|
}
|
|
rel := strings.TrimPrefix(imageURL, "/")
|
|
rel = strings.TrimPrefix(rel, "uploads/")
|
|
localPath := filepath.Join(base, filepath.FromSlash(rel))
|
|
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 - use custom client and headers, some CDNs block default Go UA
|
|
client := &http.Client{Timeout: 20 * time.Second}
|
|
req, err := http.NewRequest("GET", imageURL, nil)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
// Set a recognizable UA; align with other proxy endpoints
|
|
req.Header.Set("User-Agent", "fotbal-club/1.0")
|
|
req.Header.Set("Accept", "image/*")
|
|
// Some providers (e.g. Zonerama) may require a referer
|
|
if strings.Contains(strings.ToLower(imageURL), "zonerama.com") {
|
|
req.Header.Set("Referer", "https://zonerama.com/")
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to fetch image: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
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 (use configured UploadDir)
|
|
uploadsDir := config.AppConfig.UploadDir
|
|
if strings.TrimSpace(uploadsDir) == "" {
|
|
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
|
|
}
|
|
|
|
scheme := "http"
|
|
if c.Request.TLS != nil || strings.EqualFold(c.Request.Header.Get("X-Forwarded-Proto"), "https") {
|
|
scheme = "https"
|
|
}
|
|
host := c.Request.Host
|
|
if xf := strings.TrimSpace(c.Request.Header.Get("X-Forwarded-Host")); xf != "" {
|
|
xfl := strings.ToLower(xf)
|
|
if !strings.Contains(xfl, ":3000") && !strings.HasPrefix(xfl, "localhost:") && !strings.HasPrefix(xfl, "127.0.0.1:") {
|
|
host = xf
|
|
}
|
|
}
|
|
absolute := scheme + "://" + host + outputPath
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"url": outputPath,
|
|
"absolute_url": absolute,
|
|
})
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
scheme := "http"
|
|
if c.Request.TLS != nil || strings.EqualFold(c.Request.Header.Get("X-Forwarded-Proto"), "https") {
|
|
scheme = "https"
|
|
}
|
|
host := c.Request.Host
|
|
if xf := strings.TrimSpace(c.Request.Header.Get("X-Forwarded-Host")); xf != "" {
|
|
parts := strings.Split(xf, ",")
|
|
if len(parts) > 0 {
|
|
h := strings.TrimSpace(parts[0])
|
|
if h != "" { host = h }
|
|
}
|
|
}
|
|
if !strings.Contains(host, ":") {
|
|
if xfp := strings.TrimSpace(c.Request.Header.Get("X-Forwarded-Port")); xfp != "" {
|
|
if (scheme == "http" && xfp != "80") || (scheme == "https" && xfp != "443") {
|
|
host = host + ":" + xfp
|
|
}
|
|
}
|
|
}
|
|
absolute := scheme + "://" + host + outputPath
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"url": outputPath,
|
|
"absolute_url": absolute,
|
|
})
|
|
}
|