package file import ( "bytes" "image" "image/color" "image/draw" "image/jpeg" _ "image/png" "math" "path" "strings" xdraw "golang.org/x/image/draw" _ "golang.org/x/image/webp" ) const ( ImageVariantThumb = "thumb" ImageVariantMedium = "medium" ) const ( thumbMaxSide = 480 mediumMaxSide = 1280 ) type generatedImageVariant struct { Key string Content []byte ContentType string } type imageVariantConfig struct { Name string MaxSide int Quality int } var imageVariantConfigs = []imageVariantConfig{ {Name: ImageVariantThumb, MaxSide: thumbMaxSide, Quality: 76}, {Name: ImageVariantMedium, MaxSide: mediumMaxSide, Quality: 82}, } func ImageVariantKey(key string, variant string) string { ext := path.Ext(key) base := strings.TrimSuffix(key, ext) if base == "" { base = key } return base + "." + variant + ".jpg" } func ImageVariantFallbackKeys(key string, variant string) []string { if key == "" { return nil } keys := []string{ImageVariantKey(key, variant)} if strings.HasSuffix(key, ".medium.jpg") && variant == ImageVariantThumb { keys = append(keys, strings.TrimSuffix(key, ".medium.jpg")+"."+ImageVariantThumb+".jpg") } keys = append(keys, key) return dedupeKeys(keys) } func generateImageVariants(key string, data []byte, contentType string) []generatedImageVariant { if !strings.HasPrefix(contentType, "image/") || contentType == "image/svg+xml" { return nil } source, _, err := image.Decode(bytes.NewReader(data)) if err != nil { return nil } var variants []generatedImageVariant for _, config := range imageVariantConfigs { rendered := resizeForVariant(source, config.MaxSide) encoded, err := encodeJPEG(rendered, config.Quality) if err != nil { continue } variants = append(variants, generatedImageVariant{ Key: ImageVariantKey(key, config.Name), Content: encoded, ContentType: "image/jpeg", }) } return variants } func resizeForVariant(source image.Image, maxSide int) image.Image { bounds := source.Bounds() width := bounds.Dx() height := bounds.Dy() if width <= 0 || height <= 0 || maxSide <= 0 { return source } scale := math.Min(float64(maxSide)/float64(width), float64(maxSide)/float64(height)) if scale > 1 { scale = 1 } targetWidth := max(1, int(math.Round(float64(width)*scale))) targetHeight := max(1, int(math.Round(float64(height)*scale))) target := image.NewRGBA(image.Rect(0, 0, targetWidth, targetHeight)) draw.Draw(target, target.Bounds(), &image.Uniform{C: color.White}, image.Point{}, draw.Src) xdraw.ApproxBiLinear.Scale(target, target.Bounds(), source, bounds, draw.Over, nil) return target } func encodeJPEG(source image.Image, quality int) ([]byte, error) { var buffer bytes.Buffer err := jpeg.Encode(&buffer, source, &jpeg.Options{Quality: quality}) if err != nil { return nil, err } return buffer.Bytes(), nil } func dedupeKeys(keys []string) []string { seen := make(map[string]struct{}, len(keys)) result := make([]string, 0, len(keys)) for _, key := range keys { if key == "" { continue } if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} result = append(result, key) } return result }