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 }