Files
2026-07-13 13:00:08 +08:00

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
}