bubble up the mime type

This commit is contained in:
2025-07-21 23:14:55 -04:00
parent e0adc40661
commit 31766134ef
2 changed files with 21 additions and 35 deletions

View File

@@ -90,7 +90,7 @@ func (a anthropic) requestToAnthropicRequest(req Request) anth.MessagesRequest {
// Check if image size exceeds 5MiB (5242880 bytes) // Check if image size exceeds 5MiB (5242880 bytes)
if len(raw) >= 5242880 { if len(raw) >= 5242880 {
compressed, err := utils.CompressImage(img.Base64, 5*1024*1024) compressed, mime, err := utils.CompressImage(img.Base64, 5*1024*1024)
// just replace the image with the compressed one // just replace the image with the compressed one
if err != nil { if err != nil {
@@ -98,6 +98,7 @@ func (a anthropic) requestToAnthropicRequest(req Request) anth.MessagesRequest {
} }
img.Base64 = compressed img.Base64 = compressed
img.ContentType = mime
} }
m.Content = append(m.Content, anth.NewImageMessageContent( m.Content = append(m.Content, anth.NewImageMessageContent(

View File

@@ -7,7 +7,6 @@ import (
"image" "image"
"image/gif" "image/gif"
"image/jpeg" "image/jpeg"
"image/png"
"net/http" "net/http"
"golang.org/x/image/draw" "golang.org/x/image/draw"
@@ -15,18 +14,21 @@ import (
// CompressImage takes a base64encoded image (JPEG, PNG or GIF) and returns // CompressImage takes a base64encoded image (JPEG, PNG or GIF) and returns
// a base64encoded version that is at most maxLength in size, or an error. // a base64encoded version that is at most maxLength in size, or an error.
func CompressImage(b64 string, maxLength int) (string, error) { func CompressImage(b64 string, maxLength int) (string, string, error) {
raw, err := base64.StdEncoding.DecodeString(b64) raw, err := base64.StdEncoding.DecodeString(b64)
if err != nil { if err != nil {
return "", fmt.Errorf("base64 decode: %w", err) return "", "", fmt.Errorf("base64 decode: %w", err)
}
if len(raw) <= maxLength {
return b64, nil // small enough already
} }
switch mime := http.DetectContentType(raw); mime { mime := http.DetectContentType(raw)
if len(raw) <= maxLength {
return b64, mime, nil // small enough already
}
switch mime {
case "image/gif": case "image/gif":
return compressGIF(raw, maxLength) return compressGIF(raw, maxLength)
default: // jpeg, png, webp, etc. → treat as raster default: // jpeg, png, webp, etc. → treat as raster
return compressRaster(raw, maxLength) return compressRaster(raw, maxLength)
} }
@@ -34,20 +36,20 @@ func CompressImage(b64 string, maxLength int) (string, error) {
// ---------- Raster path (jpeg / png / singleframe gif) ---------- // ---------- Raster path (jpeg / png / singleframe gif) ----------
func compressRaster(src []byte, maxLength int) (string, error) { func compressRaster(src []byte, maxLength int) (string, string, error) {
img, _, err := image.Decode(bytes.NewReader(src)) img, _, err := image.Decode(bytes.NewReader(src))
if err != nil { if err != nil {
return "", fmt.Errorf("decode raster: %w", err) return "", "", fmt.Errorf("decode raster: %w", err)
} }
quality := 95 quality := 95
for { for {
var buf bytes.Buffer var buf bytes.Buffer
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality}); err != nil { if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality}); err != nil {
return "", fmt.Errorf("jpeg encode: %w", err) return "", "", fmt.Errorf("jpeg encode: %w", err)
} }
if buf.Len() <= maxLength { if buf.Len() <= maxLength {
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil return base64.StdEncoding.EncodeToString(buf.Bytes()), "image/jpeg", nil
} }
if quality > 20 { if quality > 20 {
@@ -58,7 +60,7 @@ func compressRaster(src []byte, maxLength int) (string, error) {
// downscale 80% // downscale 80%
b := img.Bounds() b := img.Bounds()
if b.Dx() < 100 || b.Dy() < 100 { if b.Dx() < 100 || b.Dy() < 100 {
return "", fmt.Errorf("cannot compress below %.02fMiB without destroying image", float64(maxLength)/1048576.0) return "", "", fmt.Errorf("cannot compress below %.02fMiB without destroying image", float64(maxLength)/1048576.0)
} }
dst := image.NewRGBA(image.Rect(0, 0, int(float64(b.Dx())*0.8), int(float64(b.Dy())*0.8))) dst := image.NewRGBA(image.Rect(0, 0, int(float64(b.Dx())*0.8), int(float64(b.Dy())*0.8)))
draw.ApproxBiLinear.Scale(dst, dst.Bounds(), img, b, draw.Over, nil) draw.ApproxBiLinear.Scale(dst, dst.Bounds(), img, b, draw.Over, nil)
@@ -69,25 +71,25 @@ func compressRaster(src []byte, maxLength int) (string, error) {
// ---------- Animated GIF path ---------- // ---------- Animated GIF path ----------
func compressGIF(src []byte, maxLength int) (string, error) { func compressGIF(src []byte, maxLength int) (string, string, error) {
g, err := gif.DecodeAll(bytes.NewReader(src)) g, err := gif.DecodeAll(bytes.NewReader(src))
if err != nil { if err != nil {
return "", fmt.Errorf("gif decode: %w", err) return "", "", fmt.Errorf("gif decode: %w", err)
} }
for { for {
var buf bytes.Buffer var buf bytes.Buffer
if err := gif.EncodeAll(&buf, g); err != nil { if err := gif.EncodeAll(&buf, g); err != nil {
return "", fmt.Errorf("gif encode: %w", err) return "", "", fmt.Errorf("gif encode: %w", err)
} }
if buf.Len() <= maxLength { if buf.Len() <= maxLength {
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil return base64.StdEncoding.EncodeToString(buf.Bytes()), "image/gif", nil
} }
// downscale every frame by 80% // downscale every frame by 80%
w, h := g.Config.Width, g.Config.Height w, h := g.Config.Width, g.Config.Height
if w < 100 || h < 100 { if w < 100 || h < 100 {
return "", fmt.Errorf("cannot compress animated GIF below 5MiB without excessive quality loss") return "", "", fmt.Errorf("cannot compress animated GIF below 5 MiB without excessive quality loss")
} }
nw, nh := int(float64(w)*0.8), int(float64(h)*0.8) nw, nh := int(float64(w)*0.8), int(float64(h)*0.8)
@@ -110,20 +112,3 @@ func compressGIF(src []byte, maxLength int) (string, error) {
// loop back and test size again … // loop back and test size again …
} }
} }
// ---------- Helpers for precise MIME decodes (optional) ----------
func decode(r *bytes.Reader, mime string) (image.Image, error) {
switch mime {
case "image/jpeg":
return jpeg.Decode(r)
case "image/png":
return png.Decode(r)
case "image/gif":
// for singleframe GIFs only call DecodeAll in caller if animated
return gif.Decode(r)
default:
i, _, err := image.Decode(r)
return i, err // let the stdlib guess the format
}
}