package thumbnails import ( "bytes" "context" "fmt" "image" _ "image/gif" "image/jpeg" _ "image/png" _ "golang.org/x/image/webp" // SupportedMIME nennt WebP – ohne den Dekodierer entstand dafür nie ein Vorschaubild "io" "strings" "golang.org/x/image/draw" "aether/internal/storage" ) const ( maxWidth = 256 maxHeight = 256 ) // SupportedMIME returns true if we can generate a thumbnail for this MIME type. func SupportedMIME(mime string) bool { m := strings.ToLower(mime) return strings.HasPrefix(m, "image/jpeg") || strings.HasPrefix(m, "image/png") || strings.HasPrefix(m, "image/gif") || strings.HasPrefix(m, "image/webp") } // ThumbnailKey returns the storage key for a file's thumbnail. func ThumbnailKey(fileID string) string { return "thumbs/" + fileID + ".jpg" } // Generate reads an image from src, creates a thumbnail, and stores it. // Returns nil if generation succeeds, error otherwise. func Generate(ctx context.Context, st storage.Storage, fileID, mimeType string, src io.Reader) error { if !SupportedMIME(mimeType) { return fmt.Errorf("unsupported MIME type: %s", mimeType) } img, _, err := image.Decode(src) if err != nil { return fmt.Errorf("decode image: %w", err) } thumb := resize(img, maxWidth, maxHeight) var buf bytes.Buffer if err := jpeg.Encode(&buf, thumb, &jpeg.Options{Quality: 80}); err != nil { return fmt.Errorf("encode thumbnail: %w", err) } key := ThumbnailKey(fileID) if _, _, err := st.Put(ctx, key, &buf); err != nil { return fmt.Errorf("store thumbnail: %w", err) } return nil } func resize(src image.Image, maxW, maxH int) image.Image { bounds := src.Bounds() w, h := bounds.Dx(), bounds.Dy() if w == 0 || h == 0 { return src } scaleW := float64(maxW) / float64(w) scaleH := float64(maxH) / float64(h) scale := scaleW if scaleH < scale { scale = scaleH } if scale >= 1 { return src // already smaller than max } newW := int(float64(w) * scale) newH := int(float64(h) * scale) if newW < 1 { newW = 1 } if newH < 1 { newH = 1 } dst := image.NewRGBA(image.Rect(0, 0, newW, newH)) draw.BiLinear.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil) return dst }