This commit is contained in:
Tomáš Dvořák
2025-10-16 13:32:05 +02:00
commit 12cba639b9
663 changed files with 168914 additions and 0 deletions
+267
View File
@@ -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
}