ResizeImage takes raw image bytes and ensures they fit within provider limits (max 2000×2000 pixels, max 4.5 MB). If the image already fits, it is returned unchanged. Otherwise it is scaled down (preserving aspect ratio) and re-encoded. The function tries to produce the smallest output by comparing
(data []byte, mimeType string)
| 71 | // The function tries to produce the smallest output by comparing PNG and JPEG |
| 72 | // encoding, and progressively reducing JPEG quality and dimensions if needed. |
| 73 | func ResizeImage(data []byte, mimeType string) (*ImageResizeResult, error) { |
| 74 | img, _, err := image.Decode(bytes.NewReader(data)) |
| 75 | if err != nil { |
| 76 | return nil, fmt.Errorf("decode image: %w", err) |
| 77 | } |
| 78 | |
| 79 | bounds := img.Bounds() |
| 80 | origW, origH := bounds.Dx(), bounds.Dy() |
| 81 | |
| 82 | // Guard against decompression bombs: reject images whose decoded |
| 83 | // dimensions are absurdly large. A small compressed file can expand |
| 84 | // to hundreds of megabytes in memory (e.g. 20000×20000×4 ≈ 1.6 GB). |
| 85 | if origW > maxDecodedDimension || origH > maxDecodedDimension { |
| 86 | return nil, fmt.Errorf("image dimensions too large: %dx%d (max %d)", origW, origH, maxDecodedDimension) |
| 87 | } |
| 88 | |
| 89 | // If the image already fits within all limits, return unchanged. |
| 90 | if origW <= MaxImageDimension && origH <= MaxImageDimension && len(data) <= MaxImageBytes { |
| 91 | return &ImageResizeResult{ |
| 92 | Data: data, |
| 93 | MimeType: mimeType, |
| 94 | OriginalWidth: origW, |
| 95 | OriginalHeight: origH, |
| 96 | Width: origW, |
| 97 | Height: origH, |
| 98 | Resized: false, |
| 99 | }, nil |
| 100 | } |
| 101 | |
| 102 | // Scale down to fit within MaxImageDimension, preserving aspect ratio. |
| 103 | newW, newH := fitDimensions(origW, origH, MaxImageDimension, MaxImageDimension) |
| 104 | resized := scaleImage(img, newW, newH) |
| 105 | |
| 106 | // Try both PNG and JPEG at default quality, pick the smaller one. |
| 107 | best, bestMime, err := pickSmallestEncoding(resized) |
| 108 | if err != nil { |
| 109 | return nil, fmt.Errorf("picking smallest encoding: %w", err) |
| 110 | } |
| 111 | |
| 112 | // If still over the byte limit, try JPEG with decreasing quality. |
| 113 | if len(best) > MaxImageBytes { |
| 114 | for _, q := range []int{70, 55, 40} { |
| 115 | encoded, err := encodeJPEG(resized, q) |
| 116 | if err != nil { |
| 117 | slog.Debug("JPEG encoding failed", "quality", q, "error", err) |
| 118 | continue |
| 119 | } |
| 120 | |
| 121 | if len(encoded) < len(best) { |
| 122 | best = encoded |
| 123 | bestMime = "image/jpeg" |
| 124 | } |
| 125 | if len(best) <= MaxImageBytes { |
| 126 | break |
| 127 | } |
| 128 | } |
| 129 | } |
| 130 |