package imageutil import ( "bytes" "fmt" "image" "image/jpeg" "image/png" "io" "strings" "golang.org/x/image/draw" ) type ResizeOptions struct { MaxWidth int Quality int // JPEG quality 1-100 } func DefaultResizeOptions() ResizeOptions { return ResizeOptions{MaxWidth: 800, Quality: 80} } // GenerateThumbnail reads image data, resizes if wider than MaxWidth, and // re-encodes as JPEG. Returns the thumbnail bytes and whether the image // was actually resized (false if already small enough but still re-encoded). func GenerateThumbnail(data []byte, contentType string, opts ResizeOptions) ([]byte, error) { if opts.MaxWidth <= 0 { opts.MaxWidth = 800 } if opts.Quality <= 0 || opts.Quality > 100 { opts.Quality = 80 } src, err := decodeImage(bytes.NewReader(data), contentType) if err != nil { return nil, fmt.Errorf("decode image: %w", err) } bounds := src.Bounds() srcW := bounds.Dx() srcH := bounds.Dy() dstW := srcW dstH := srcH if srcW > opts.MaxWidth { dstW = opts.MaxWidth dstH = srcH * opts.MaxWidth / srcW } dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH)) draw.BiLinear.Scale(dst, dst.Bounds(), src, bounds, draw.Over, nil) var buf bytes.Buffer if err := jpeg.Encode(&buf, dst, &jpeg.Options{Quality: opts.Quality}); err != nil { return nil, fmt.Errorf("encode jpeg: %w", err) } return buf.Bytes(), nil } func decodeImage(r io.Reader, contentType string) (image.Image, error) { ct := strings.ToLower(contentType) switch { case strings.Contains(ct, "png"): return png.Decode(r) case strings.Contains(ct, "jpeg"), strings.Contains(ct, "jpg"): return jpeg.Decode(r) default: img, _, err := image.Decode(r) return img, err } }