package main import ( "bytes" "context" "encoding/base64" "fmt" "image" "image/png" _ "image/gif" _ "image/jpeg" _ "github.com/biessek/golang-ico" _ "golang.org/x/image/webp" ) // convertIconToBase64PNG downloads an icon from S3, converts it to PNG, and returns base64-encoded data. func convertIconToBase64PNG(ctx context.Context, s3Key string, iconsBucket string) (encoded string, width, height int, err error) { data, err := s3Download(ctx, iconsBucket, s3Key) if err != nil { return "", 0, 0, fmt.Errorf("s3 download: %w", err) } // Check for SVG (can't decode to raster without external deps) if isSVG(data) { return "", 0, 0, fmt.Errorf("svg not supported") } // image.Decode handles PNG, GIF, JPEG, WebP, BMP, and ICO (via registered decoders) img, _, err := image.Decode(bytes.NewReader(data)) if err != nil { return "", 0, 0, fmt.Errorf("decode: %w", err) } // Downscale icons >128px to 32x32 to keep bundle sizes reasonable bounds := img.Bounds() w, h := bounds.Dx(), bounds.Dy() if w > 128 || h > 128 { img = resizeNearestNeighbor(img, 32, 32) w, h = 32, 32 } // Re-encode as PNG var pngBuf bytes.Buffer if err := png.Encode(&pngBuf, img); err != nil { return "", 0, 0, fmt.Errorf("png encode: %w", err) } encoded = base64.StdEncoding.EncodeToString(pngBuf.Bytes()) return encoded, w, h, nil } // resizeNearestNeighbor does a simple nearest-neighbor resize. func resizeNearestNeighbor(src image.Image, dstW, dstH int) image.Image { srcBounds := src.Bounds() srcW := srcBounds.Dx() srcH := srcBounds.Dy() dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH)) for y := 0; y < dstH; y++ { srcY := srcBounds.Min.Y + y*srcH/dstH for x := 0; x < dstW; x++ { srcX := srcBounds.Min.X + x*srcW/dstW dst.Set(x, y, src.At(srcX, srcY)) } } return dst } func isSVG(data []byte) bool { if len(data) < 5 { return false } header := data[:min(256, len(data))] return bytes.Contains(header, []byte("