mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
upload
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user