mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
268 lines
5.7 KiB
Go
268 lines
5.7 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// Note: To use this image optimizer, install the required package:
|
|
// go get golang.org/x/image/draw
|
|
//
|
|
// For now, we'll use basic resizing without the external library
|
|
// Uncomment the import above and the advanced resize function when ready
|
|
|
|
// ImageSize defines thumbnail dimensions
|
|
type ImageSize struct {
|
|
Width int
|
|
Height int
|
|
Name string
|
|
}
|
|
|
|
var (
|
|
// StandardSizes for responsive images
|
|
StandardSizes = []ImageSize{
|
|
{Width: 150, Height: 150, Name: "thumb"},
|
|
{Width: 400, Height: 400, Name: "small"},
|
|
{Width: 800, Height: 800, Name: "medium"},
|
|
{Width: 1200, Height: 1200, Name: "large"},
|
|
}
|
|
)
|
|
|
|
// OptimizedImage holds paths to all generated sizes
|
|
type OptimizedImage struct {
|
|
Original string
|
|
Thumb string
|
|
Small string
|
|
Medium string
|
|
Large string
|
|
}
|
|
|
|
// OptimizeAndResize processes an uploaded image
|
|
func OptimizeAndResize(sourcePath string) (*OptimizedImage, error) {
|
|
// Open source image
|
|
file, err := os.Open(sourcePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open image: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
// Decode image
|
|
img, format, err := image.Decode(file)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode image: %w", err)
|
|
}
|
|
|
|
result := &OptimizedImage{
|
|
Original: sourcePath,
|
|
}
|
|
|
|
// Generate thumbnails
|
|
dir := filepath.Dir(sourcePath)
|
|
base := strings.TrimSuffix(filepath.Base(sourcePath), filepath.Ext(sourcePath))
|
|
|
|
for _, size := range StandardSizes {
|
|
resized := resizeImage(img, size.Width, size.Height)
|
|
outputPath := filepath.Join(dir, fmt.Sprintf("%s_%s.jpg", base, size.Name))
|
|
|
|
if err := saveJPEG(resized, outputPath, 85); err != nil {
|
|
continue
|
|
}
|
|
|
|
// Store path in result
|
|
switch size.Name {
|
|
case "thumb":
|
|
result.Thumb = outputPath
|
|
case "small":
|
|
result.Small = outputPath
|
|
case "medium":
|
|
result.Medium = outputPath
|
|
case "large":
|
|
result.Large = outputPath
|
|
}
|
|
}
|
|
|
|
// Optimize original if it's too large
|
|
if format == "jpeg" || format == "jpg" {
|
|
optimizeJPEG(sourcePath)
|
|
} else if format == "png" {
|
|
convertPNGToJPEG(sourcePath)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// resizeImage resizes image maintaining aspect ratio
|
|
// Using simple nearest-neighbor scaling (for production, consider golang.org/x/image/draw)
|
|
func resizeImage(src image.Image, maxWidth, maxHeight int) image.Image {
|
|
srcBounds := src.Bounds()
|
|
srcW := srcBounds.Dx()
|
|
srcH := srcBounds.Dy()
|
|
|
|
// Calculate new dimensions maintaining aspect ratio
|
|
ratio := float64(srcW) / float64(srcH)
|
|
var newW, newH int
|
|
|
|
if srcW > srcH {
|
|
newW = maxWidth
|
|
newH = int(float64(maxWidth) / ratio)
|
|
} else {
|
|
newH = maxHeight
|
|
newW = int(float64(maxHeight) * ratio)
|
|
}
|
|
|
|
// Don't upscale
|
|
if newW > srcW || newH > srcH {
|
|
return src
|
|
}
|
|
|
|
// Simple nearest-neighbor resize
|
|
// For production quality, use: golang.org/x/image/draw with CatmullRom
|
|
dst := image.NewRGBA(image.Rect(0, 0, newW, newH))
|
|
xRatio := float64(srcW) / float64(newW)
|
|
yRatio := float64(srcH) / float64(newH)
|
|
|
|
for y := 0; y < newH; y++ {
|
|
for x := 0; x < newW; x++ {
|
|
srcX := int(float64(x) * xRatio)
|
|
srcY := int(float64(y) * yRatio)
|
|
dst.Set(x, y, src.At(srcX, srcY))
|
|
}
|
|
}
|
|
|
|
return dst
|
|
}
|
|
|
|
// saveJPEG saves image as JPEG with specified quality
|
|
func saveJPEG(img image.Image, path string, quality int) error {
|
|
out, err := os.Create(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
return jpeg.Encode(out, img, &jpeg.Options{Quality: quality})
|
|
}
|
|
|
|
// optimizeJPEG re-encodes JPEG with optimal quality
|
|
func optimizeJPEG(path string) error {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
img, err := jpeg.Decode(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create temporary file
|
|
tmpPath := path + ".tmp"
|
|
out, err := os.Create(tmpPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Encode with 85% quality
|
|
err = jpeg.Encode(out, img, &jpeg.Options{Quality: 85})
|
|
out.Close()
|
|
|
|
if err != nil {
|
|
os.Remove(tmpPath)
|
|
return err
|
|
}
|
|
|
|
// Check if new file is smaller
|
|
origInfo, _ := os.Stat(path)
|
|
newInfo, _ := os.Stat(tmpPath)
|
|
|
|
if newInfo.Size() < origInfo.Size() {
|
|
// Replace original
|
|
os.Remove(path)
|
|
os.Rename(tmpPath, path)
|
|
} else {
|
|
// Keep original
|
|
os.Remove(tmpPath)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// convertPNGToJPEG converts PNG to JPEG for better compression
|
|
func convertPNGToJPEG(path string) error {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
img, err := png.Decode(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create JPEG version
|
|
jpegPath := strings.TrimSuffix(path, ".png") + ".jpg"
|
|
out, err := os.Create(jpegPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
return jpeg.Encode(out, img, &jpeg.Options{Quality: 90})
|
|
}
|
|
|
|
// GetImageDimensions returns width and height of an image
|
|
func GetImageDimensions(path string) (int, int, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
defer file.Close()
|
|
|
|
config, _, err := image.DecodeConfig(file)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
return config.Width, config.Height, nil
|
|
}
|
|
|
|
// ValidateImageFile checks if file is a valid image
|
|
func ValidateImageFile(reader io.Reader) (string, error) {
|
|
// Read first 512 bytes for MIME detection
|
|
buf := make([]byte, 512)
|
|
n, err := io.ReadFull(reader, buf)
|
|
if err != nil && err != io.ErrUnexpectedEOF {
|
|
return "", err
|
|
}
|
|
|
|
// Try to decode as image
|
|
_, format, err := image.Decode(bytes.NewReader(buf[:n]))
|
|
if err != nil {
|
|
return "", fmt.Errorf("not a valid image: %w", err)
|
|
}
|
|
|
|
// Validate format
|
|
validFormats := map[string]bool{
|
|
"jpeg": true,
|
|
"jpg": true,
|
|
"png": true,
|
|
"gif": true,
|
|
"webp": true,
|
|
}
|
|
|
|
if !validFormats[format] {
|
|
return "", fmt.Errorf("unsupported image format: %s", format)
|
|
}
|
|
|
|
return format, nil
|
|
}
|