78 lines
2.3 KiB
Go
78 lines
2.3 KiB
Go
package control
|
|
|
|
import (
|
|
"bytes"
|
|
"image"
|
|
_ "image/gif" // register gif decoder
|
|
"image/jpeg"
|
|
"image/png"
|
|
|
|
xdraw "golang.org/x/image/draw"
|
|
_ "golang.org/x/image/webp" // register webp decoder
|
|
)
|
|
|
|
// maxVisionDim caps the longest image side sent to a model. OpenAI and Anthropic
|
|
// downscale to roughly this server-side anyway, so a larger upload only wastes
|
|
// request bytes and image tokens without adding fidelity.
|
|
const maxVisionDim = 1568
|
|
|
|
// maxDecodePixels guards against decompression-bomb attachments: a tiny file can
|
|
// declare enormous dimensions. Beyond this we skip decoding and send as-is (still
|
|
// bounded by the 10 MB file cap).
|
|
const maxDecodePixels = 50_000_000
|
|
|
|
// compressForVision downscales an oversized image to maxVisionDim and re-encodes
|
|
// it — PNG/GIF stay lossless (screenshots, text, transparency), JPEG/WebP go to
|
|
// JPEG. Best-effort: an undecodable format, a decode/encode failure, or an image
|
|
// already within budget returns the original bytes and mime unchanged.
|
|
func compressForVision(raw []byte, mime string) ([]byte, string) {
|
|
switch mime {
|
|
case "image/png", "image/jpeg", "image/gif", "image/webp":
|
|
default:
|
|
return raw, mime // bmp/tiff/svg: no decoder wired, send original
|
|
}
|
|
cfg, _, err := image.DecodeConfig(bytes.NewReader(raw))
|
|
if err != nil || cfg.Width*cfg.Height > maxDecodePixels {
|
|
return raw, mime
|
|
}
|
|
if cfg.Width <= maxVisionDim && cfg.Height <= maxVisionDim {
|
|
return raw, mime // within budget — no point re-encoding
|
|
}
|
|
src, _, err := image.Decode(bytes.NewReader(raw))
|
|
if err != nil {
|
|
return raw, mime
|
|
}
|
|
w, h := scaledDims(cfg.Width, cfg.Height, maxVisionDim)
|
|
dst := image.NewRGBA(image.Rect(0, 0, w, h))
|
|
xdraw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), xdraw.Over, nil)
|
|
|
|
var buf bytes.Buffer
|
|
if mime == "image/png" || mime == "image/gif" {
|
|
if err := png.Encode(&buf, dst); err != nil {
|
|
return raw, mime
|
|
}
|
|
return buf.Bytes(), "image/png"
|
|
}
|
|
if err := jpeg.Encode(&buf, dst, &jpeg.Options{Quality: 85}); err != nil {
|
|
return raw, mime
|
|
}
|
|
return buf.Bytes(), "image/jpeg"
|
|
}
|
|
|
|
// scaledDims returns dimensions with the longest side clamped to m, preserving
|
|
// aspect ratio (each side at least 1px).
|
|
func scaledDims(w, h, m int) (int, int) {
|
|
if w >= h {
|
|
nh := h * m / w
|
|
if nh < 1 {
|
|
nh = 1
|
|
}
|
|
return m, nh
|
|
}
|
|
nw := w * m / h
|
|
if nw < 1 {
|
|
nw = 1
|
|
}
|
|
return nw, m
|
|
}
|