1package provider
2
3import (
4 "bytes"
5 "image"
6 "image/jpeg"
7 "image/png"
8 "strings"
9
10 xdraw "golang.org/x/image/draw"
11
12 // Register decoders for the formats we want to accept on input.
13 _ "image/gif"
14 _ "image/jpeg"
15 _ "image/png"
16)
17
18// anthMaxImageSide is Anthropic's per-image dimension cap when a
19// request contains more than one image. Single-image requests are
20// allowed up to 8000 px, but zot conversations routinely include
21// multiple images (a screenshot here, a tool result image there)
22// so we always normalise down to the stricter limit. 2000 px on the
23// longest side is well below their cap and still readable for the
24// kinds of screenshots / charts the model usually consumes.
25const anthMaxImageSide = 2000
26
27// anthShrinkImageBytesIfTooBig returns data unchanged when the image
28// already fits within Anthropic's per-image dimension cap. When it
29// doesn't, the image is decoded, resampled with Catmull-Rom (a good
30// balance of quality and speed for downscaling), and re-encoded in
31// the same format. The MIME type may be rewritten when re-encoding
32// requires a format change (e.g. an image originally tagged image/gif
33// is shipped as image/png after resize, since stdlib only encodes
34// gif at the package's own narrow API which is awkward for a single
35// frame).
36//
37// Errors during decode/encode return the original bytes untouched so
38// the caller's existing flow continues to work; Anthropic will then
39// emit its own clearer error than we could synthesise. We log no
40// failures here because this runs on every outbound request and is
41// best-effort.
42func anthShrinkImageBytesIfTooBig(data []byte, mime string) ([]byte, string) {
43 if len(data) == 0 {
44 return data, mime
45 }
46 cfg, format, err := image.DecodeConfig(bytes.NewReader(data))
47 if err != nil {
48 return data, mime
49 }
50 if cfg.Width <= anthMaxImageSide && cfg.Height <= anthMaxImageSide {
51 return data, mime
52 }
53 // Compute target dimensions preserving aspect ratio, longest side
54 // clamped to anthMaxImageSide.
55 tw, th := cfg.Width, cfg.Height
56 if tw >= th {
57 th = th * anthMaxImageSide / tw
58 tw = anthMaxImageSide
59 } else {
60 tw = tw * anthMaxImageSide / th
61 th = anthMaxImageSide
62 }
63 if tw < 1 {
64 tw = 1
65 }
66 if th < 1 {
67 th = 1
68 }
69
70 src, _, err := image.Decode(bytes.NewReader(data))
71 if err != nil {
72 return data, mime
73 }
74 dst := image.NewRGBA(image.Rect(0, 0, tw, th))
75 xdraw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), xdraw.Over, nil)
76
77 var buf bytes.Buffer
78 switch strings.ToLower(format) {
79 case "jpeg":
80 if err := jpeg.Encode(&buf, dst, &jpeg.Options{Quality: 85}); err != nil {
81 return data, mime
82 }
83 return buf.Bytes(), "image/jpeg"
84 case "png", "gif":
85 // gifs are re-encoded as png so the call site stays simple
86 // (avoiding a single-frame Encode helper). Anthropic accepts
87 // image/png happily; the visual result is the same for the
88 // model's vision pipeline.
89 if err := png.Encode(&buf, dst); err != nil {
90 return data, mime
91 }
92 nextMime := mime
93 if strings.ToLower(format) == "gif" {
94 nextMime = "image/png"
95 }
96 return buf.Bytes(), nextMime
97 default:
98 return data, mime
99 }
100}