返回 DeepSeek-Reasonix
imagecompress.go
根目录 / internal / control / imagecompress.go
1 package control
2
3 import (
4 "bytes"
5 "image"
6 _ "image/gif" // register gif decoder
7 "image/jpeg"
8 "image/png"
9
10 xdraw "golang.org/x/image/draw"
11 _ "golang.org/x/image/webp" // register webp decoder
12 )
13
14 // maxVisionDim caps the longest image side sent to a model. OpenAI and Anthropic
15 // downscale to roughly this server-side anyway, so a larger upload only wastes
16 // request bytes and image tokens without adding fidelity.
17 const maxVisionDim = 1568
18
19 // maxDecodePixels guards against decompression-bomb attachments: a tiny file can
20 // declare enormous dimensions. Beyond this we skip decoding and send as-is (still
21 // bounded by the 10 MB file cap).
22 const maxDecodePixels = 50_000_000
23
24 // compressForVision downscales an oversized image to maxVisionDim and re-encodes
25 // it — PNG/GIF stay lossless (screenshots, text, transparency), JPEG/WebP go to
26 // JPEG. Best-effort: an undecodable format, a decode/encode failure, or an image
27 // already within budget returns the original bytes and mime unchanged.
28 func compressForVision(raw []byte, mime string) ([]byte, string) {
29 switch mime {
30 case "image/png", "image/jpeg", "image/gif", "image/webp":
31 default:
32 return raw, mime // bmp/tiff/svg: no decoder wired, send original
33 }
34 cfg, _, err := image.DecodeConfig(bytes.NewReader(raw))
35 if err != nil || cfg.Width*cfg.Height > maxDecodePixels {
36 return raw, mime
37 }
38 if cfg.Width <= maxVisionDim && cfg.Height <= maxVisionDim {
39 return raw, mime // within budget — no point re-encoding
40 }
41 src, _, err := image.Decode(bytes.NewReader(raw))
42 if err != nil {
43 return raw, mime
44 }
45 w, h := scaledDims(cfg.Width, cfg.Height, maxVisionDim)
46 dst := image.NewRGBA(image.Rect(0, 0, w, h))
47 xdraw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), xdraw.Over, nil)
48
49 var buf bytes.Buffer
50 if mime == "image/png" || mime == "image/gif" {
51 if err := png.Encode(&buf, dst); err != nil {
52 return raw, mime
53 }
54 return buf.Bytes(), "image/png"
55 }
56 if err := jpeg.Encode(&buf, dst, &jpeg.Options{Quality: 85}); err != nil {
57 return raw, mime
58 }
59 return buf.Bytes(), "image/jpeg"
60 }
61
62 // scaledDims returns dimensions with the longest side clamped to m, preserving
63 // aspect ratio (each side at least 1px).
64 func scaledDims(w, h, m int) (int, int) {
65 if w >= h {
66 nh := h * m / w
67 if nh < 1 {
68 nh = 1
69 }
70 return m, nh
71 }
72 nw := w * m / h
73 if nw < 1 {
74 nw = 1
75 }
76 return nw, m
77 }
78
78 lines GO