package tools import ( "fmt" "image" "image/color" "image/jpeg" _ "image/png" // 解码 PNG 截图 "os" "time" ) // 视觉压缩策略:视觉模型会把图片缩到 ~1568px 再处理,原始 Retina 全屏 PNG // (2-3MB)纯属浪费且容易顶爆网关的单请求字节上限。超过阈值就降采样到 // maxVisionWidth 并转 JPEG(截图场景质量足够,体积约 1/10)。 const ( shrinkThresholdBytes = 600 << 10 // 600KB maxVisionWidth = 1280 visionJPEGQuality = 80 ) // ShrinkForVision 尽力压缩大图:PNG 且超过阈值时降采样 + 转 JPEG, // 返回新路径;小图或解码失败时原样返回(调用方无需关心错误)。 func ShrinkForVision(path string) (string, error) { st, err := os.Stat(path) if err != nil || st.Size() <= shrinkThresholdBytes { return path, nil } f, err := os.Open(path) if err != nil { return path, nil } src, _, err := image.Decode(f) f.Close() if err != nil { return path, nil // 非图片/损坏:保持原样 } scaled := scaleToWidth(src, maxVisionWidth) out := fmt.Sprintf("%s/vision-%d.jpg", os.TempDir(), time.Now().UnixNano()) of, err := os.Create(out) if err != nil { return path, nil } if err := jpeg.Encode(of, scaled, &jpeg.Options{Quality: visionJPEGQuality}); err != nil { of.Close() os.Remove(out) return path, nil } if err := of.Close(); err != nil { os.Remove(out) return path, nil } // 压缩后反而更大(异常情况)则回退原图 if ost, err2 := os.Stat(out); err2 != nil || ost.Size() >= st.Size() { os.Remove(out) return path, nil } return out, nil } // scaleToWidth 等比缩到 maxW 宽(box 平均采样,stdlib 实现)。 func scaleToWidth(src image.Image, maxW int) image.Image { b := src.Bounds() w, h := b.Dx(), b.Dy() if w <= maxW || w == 0 || h == 0 { return src } nh := h * maxW / w if nh < 1 { nh = 1 } dst := image.NewRGBA(image.Rect(0, 0, maxW, nh)) for y := 0; y < nh; y++ { sy0, sy1 := y*h/nh, (y+1)*h/nh if sy1 <= sy0 { sy1 = sy0 + 1 } for x := 0; x < maxW; x++ { sx0, sx1 := x*w/maxW, (x+1)*w/maxW if sx1 <= sx0 { sx1 = sx0 + 1 } var rs, gs, bs, as, n uint64 stepY := (sy1 - sy0) / 4 if stepY < 1 { stepY = 1 } stepX := (sx1 - sx0) / 4 if stepX < 1 { stepX = 1 } for sy := sy0; sy < sy1; sy += stepY { for sx := sx0; sx < sx1; sx += stepX { r, g, bl, a := src.At(b.Min.X+sx, b.Min.Y+sy).RGBA() rs += uint64(r) gs += uint64(g) bs += uint64(bl) as += uint64(a) n++ } } if n == 0 { n = 1 } dst.Set(x, y, rgbaFrom64(rs/n, gs/n, bs/n, as/n)) } } return dst } func rgbaFrom64(r, g, b, a uint64) color.RGBA { return color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)} }